tksbrokerapi.TKSBrokerAPI

TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios, as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: from the console, it has a rich keys and commands, or you can use it as Python module with python import.

TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.

   1# -*- coding: utf-8 -*-
   2# Author: Timur Gilmullin
   3
   4"""
   5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios,
   6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
   7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`.
   8
   9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive
  10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
  11
  12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH
  13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
  14- **See CLI examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
  15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
  16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/
  17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/
  18"""
  19
  20# Copyright (c) 2022 Gilmillin Timur Mansurovich
  21#
  22# Licensed under the Apache License, Version 2.0 (the "License");
  23# you may not use this file except in compliance with the License.
  24# You may obtain a copy of the License at
  25#
  26#     http://www.apache.org/licenses/LICENSE-2.0
  27#
  28# Unless required by applicable law or agreed to in writing, software
  29# distributed under the License is distributed on an "AS IS" BASIS,
  30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  31# See the License for the specific language governing permissions and
  32# limitations under the License.
  33
  34
  35import sys
  36import os
  37from argparse import ArgumentParser
  38from importlib.metadata import version
  39
  40from dateutil.tz import tzlocal
  41from time import sleep
  42
  43import re
  44import json
  45import requests
  46import traceback as tb
  47from typing import Union
  48
  49from multiprocessing import cpu_count, Lock
  50from multiprocessing.pool import ThreadPool
  51import pandas as pd
  52
  53from mako.template import Template  # Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
  54from Templates import *  # Some html-templates used by reporting methods in TKSBrokerAPI module
  55from TKSEnums import *  # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/
  56from TradeRoutines import *  # This library contains some methods used by trade scenarios implemented with TKSBrokerAPI module
  57
  58from pricegenerator.PriceGenerator import PriceGenerator, uLogger  # This module has a lot of instruments to work with candles data (https://github.com/Tim55667757/PriceGenerator)
  59from pricegenerator.UniLogger import DisableLogger as PGDisLog  # Method for disable log from PriceGenerator
  60
  61import UniLogger as uLog  # Logger for TKSBrokerAPI
  62
  63
  64# --- Common technical parameters:
  65
  66PGDisLog(uLogger.handlers[0])  # Disable 3-rd party logging from PriceGenerator
  67uLogger = uLog.UniLogger  # init logger for TKSBrokerAPI
  68uLogger.level = 10  # debug level by default for TKSBrokerAPI module
  69uLogger.handlers[0].level = 20  # info level by default for STDOUT of TKSBrokerAPI module
  70
  71__version__ = "1.6"  # The "major.minor" version setup here, but build number define at the build-server only
  72
  73CPU_COUNT = cpu_count()  # host's real CPU count
  74CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1  # how many CPUs will be used for parallel calculations
  75
  76
  77class TinkoffBrokerServer:
  78    """
  79    This class implements methods to work with Tinkoff broker server.
  80
  81    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
  82
  83    About `token`: https://tinkoff.github.io/investAPI/token/
  84    """
  85    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
  86        """
  87        Main class init.
  88
  89        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
  90        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
  91                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
  92        :param useCache: use default cache file with raw data to use instead of `iList`.
  93                         True by default. Cache is auto-update if new day has come.
  94                         If you don't want to use cache and always updates raw data then set `useCache=False`.
  95        :param defaultCache: path to default cache file. `dump.json` by default.
  96        """
  97        if token is None or not token:
  98            try:
  99                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 100                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 101
 102            except KeyError:
 103                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 104                raise Exception("Token required")
 105
 106        else:
 107            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 108            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 109
 110        if accountId is None or not accountId:
 111            try:
 112                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 113                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 114
 115            except KeyError:
 116                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 117
 118        else:
 119            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 120            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 121
 122        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 123        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 124
 125        Latest version: https://pypi.org/project/tksbrokerapi/
 126        """
 127
 128        self.__lock = Lock()  # initialize multiprocessing mutex lock
 129
 130        self.aliases = TKS_TICKER_ALIASES
 131        """Some aliases instead official tickers.
 132
 133        See also: `TKSEnums.TKS_TICKER_ALIASES`
 134        """
 135
 136        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 137
 138        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 139
 140        self._ticker = ""
 141        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 142
 143        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 144        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 145
 146        See also: `SearchByTicker()`, `SearchInstruments()`.
 147        """
 148
 149        self._figi = ""
 150        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 151
 152        See also: `SearchByFIGI()`, `SearchInstruments()`.
 153        """
 154
 155        self.depth = 1
 156        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 157
 158        See also: `GetCurrentPrices()`.
 159        """
 160
 161        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 162        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 163
 164        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 165        """
 166
 167        uLogger.debug("Broker API server: {}".format(self.server))
 168
 169        self.timeout = 15
 170        """Server operations timeout in seconds. Default: `15`.
 171
 172        See also: `SendAPIRequest()`.
 173        """
 174
 175        self.headers = {
 176            "Content-Type": "application/json",
 177            "accept": "application/json",
 178            "Authorization": "Bearer {}".format(self.token),
 179            "x-app-name": "Tim55667757.TKSBrokerAPI",
 180        }
 181        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 182
 183        See also: `SendAPIRequest()`.
 184        """
 185
 186        self.body = None
 187        """Request body which send to broker server. Default: `None`.
 188
 189        See also: `SendAPIRequest()`.
 190        """
 191
 192        self.moreDebug = False
 193        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 194
 195        self.useHTMLReports = False
 196        """
 197        If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default.
 198        
 199        See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
 200        """
 201
 202        self.historyFile = None
 203        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 204
 205        See also: `History()`.
 206        """
 207
 208        self.htmlHistoryFile = "index.html"
 209        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 210
 211        See also: `ShowHistoryChart()`.
 212        """
 213
 214        self.instrumentsFile = "instruments.md"
 215        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 216
 217        See also: `ShowInstrumentsInfo()`.
 218        """
 219
 220        self.searchResultsFile = "search-results.md"
 221        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 222
 223        See also: `SearchInstruments()`.
 224        """
 225
 226        self.pricesFile = "prices.md"
 227        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 228
 229        See also: `GetListOfPrices()`.
 230        """
 231
 232        self.infoFile = "info.md"
 233        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 234
 235        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 236        """
 237
 238        self.bondsXLSXFile = "ext-bonds.xlsx"
 239        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 240        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 241
 242        See also: `ExtendBondsData()`.
 243        """
 244
 245        self.calendarFile = "calendar.md"
 246        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 247        
 248        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 249
 250        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 251        """
 252
 253        self.overviewFile = "overview.md"
 254        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 255
 256        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 257        """
 258
 259        self.overviewDigestFile = "overview-digest.md"
 260        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 261
 262        See also: `Overview()` with parameter `details="digest"`.
 263        """
 264
 265        self.overviewPositionsFile = "overview-positions.md"
 266        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 267
 268        See also: `Overview()` with parameter `details="positions"`.
 269        """
 270
 271        self.overviewOrdersFile = "overview-orders.md"
 272        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 273
 274        See also: `Overview()` with parameter `details="orders"`.
 275        """
 276
 277        self.overviewAnalyticsFile = "overview-analytics.md"
 278        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 279
 280        See also: `Overview()` with parameter `details="analytics"`.
 281        """
 282
 283        self.overviewBondsCalendarFile = "overview-calendar.md"
 284        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 285
 286        See also: `Overview()` with parameter `details="calendar"`.
 287        """
 288
 289        self.reportFile = "deals.md"
 290        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 291
 292        See also: `Deals()`.
 293        """
 294
 295        self.withdrawalLimitsFile = "limits.md"
 296        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 297
 298        See also: `OverviewLimits()` and `RequestLimits()`.
 299        """
 300
 301        self.userInfoFile = "user-info.md"
 302        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 303
 304        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 305        """
 306
 307        self.userAccountsFile = "accounts.md"
 308        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 309
 310        See also: `OverviewAccounts()`, `RequestAccounts()`.
 311        """
 312
 313        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 314        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 315
 316        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 317
 318        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 319        """
 320
 321        self.iList = None  # init iList for raw instruments data
 322        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 323        
 324        See also: `Listing()`, `DumpInstruments()`.
 325        """
 326
 327        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 328        if useCache:
 329            if os.path.exists(self.iListDumpFile):
 330                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 331                curTime = datetime.now(tzutc())
 332
 333                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 334                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 335
 336                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 337
 338                else:
 339                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 340
 341                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 342                        os.path.abspath(self.iListDumpFile),
 343                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 344                    ))
 345
 346            else:
 347                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 348                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 349
 350        else:
 351            self.iList = self.Listing()  # request new raw instruments data from broker server
 352            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 353
 354        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 355        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 356
 357        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 358        """
 359
 360    @property
 361    def ticker(self) -> str:
 362        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 363
 364        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 365        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 366
 367        See also: `SearchByTicker()`, `SearchInstruments()`.
 368        """
 369        return self._ticker
 370
 371    @ticker.setter
 372    def ticker(self, value):
 373        """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 374
 375        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 376        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 377
 378        See also: `SearchByTicker()`, `SearchInstruments()`.
 379        """
 380        self._ticker = str(value).upper()  # Tickers may be upper case only
 381
 382    @property
 383    def figi(self) -> str:
 384        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 385
 386        See also: `SearchByFIGI()`, `SearchInstruments()`.
 387        """
 388        return self._figi
 389
 390    @figi.setter
 391    def figi(self, value):
 392        """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 393
 394        See also: `SearchByFIGI()`, `SearchInstruments()`.
 395        """
 396        self._figi = str(value).upper()  # FIGI may be upper case only
 397
 398    def _ParseJSON(self, rawData="{}") -> dict:
 399        """
 400        Parse JSON from response string.
 401
 402        :param rawData: this is a string with JSON-formatted text.
 403        :return: JSON (dictionary), parsed from server response string.
 404        """
 405        responseJSON = json.loads(rawData) if rawData else {}
 406
 407        if self.moreDebug:
 408            uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 409
 410        return responseJSON
 411
 412    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 413        """
 414        Send GET or POST request to broker server and receive JSON object.
 415
 416        self.header: must be defining with dictionary of headers.
 417        self.body: if define then used as request body. None by default.
 418        self.timeout: global request timeout, 15 seconds by default.
 419        :param url: url with REST request.
 420        :param reqType: send "GET" or "POST" request. "GET" by default.
 421        :param retry: how many times retry after first request if an 5xx server errors occurred.
 422        :param pause: sleep time in seconds between retries.
 423        :return: response JSON (dictionary) from broker.
 424        """
 425        if reqType.upper() not in ("GET", "POST"):
 426            uLogger.error("You can define request type: `GET` or `POST`!")
 427            raise Exception("Incorrect value")
 428
 429        if self.moreDebug:
 430            uLogger.debug("Request parameters:")
 431            uLogger.debug("    - REST API URL: {}".format(url))
 432            uLogger.debug("    - request type: {}".format(reqType))
 433            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 434            uLogger.debug("    - body:\n{}".format(self.body))
 435
 436        # fast hack to avoid all operations with some tickers/FIGI
 437        responseJSON = {}
 438        oK = True
 439        for item in self.exclude:
 440            if item in url:
 441                if self.moreDebug:
 442                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 443
 444                oK = False
 445                break
 446
 447        if oK:
 448            with self.__lock:  # acquire the mutex lock
 449                counter = 0
 450                response = None
 451                errMsg = ""
 452
 453                while not response and counter <= retry:
 454                    if reqType == "GET":
 455                        response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 456
 457                    if reqType == "POST":
 458                        response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 459
 460                    if self.moreDebug:
 461                        uLogger.debug("Response:")
 462                        uLogger.debug("    - status code: {}".format(response.status_code))
 463                        uLogger.debug("    - reason: {}".format(response.reason))
 464                        uLogger.debug("    - body length: {}".format(len(response.text)))
 465                        uLogger.debug("    - headers:\n{}".format(response.headers))
 466
 467                    # Server returns some headers:
 468                    # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 469                    # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 470                    # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 471                    # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 472                    if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 473                        rateLimitWait = int(response.headers["x-ratelimit-reset"])
 474                        uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 475                        sleep(rateLimitWait)
 476
 477                    # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 478                    if 400 <= response.status_code < 500:
 479                        msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 480                        uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 481
 482                        if "code" in response.text and "message" in response.text:
 483                            msgDict = self._ParseJSON(rawData=response.text)
 484                            uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
 485
 486                        counter = retry + 1  # do not retry for 4xx errors
 487
 488                    if 500 <= response.status_code < 600:
 489                        errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 490                        uLogger.debug("    - not oK, {}".format(errMsg))
 491
 492                        if "code" in response.text and "message" in response.text:
 493                            errMsgDict = self._ParseJSON(rawData=response.text)
 494                            uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
 495
 496                        counter += 1
 497
 498                        if counter <= retry:
 499                            uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 500                            sleep(pause)
 501
 502                responseJSON = self._ParseJSON(rawData=response.text)
 503
 504                if errMsg:
 505                    uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 506                    uLogger.error("    - not oK, {}".format(errMsg))
 507
 508        return responseJSON
 509
 510    def _IUpdater(self, iType: str) -> tuple:
 511        """
 512        Request instrument by type from server. See available API methods for instruments:
 513        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 514        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 515        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 516        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 517        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 518
 519        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 520        :return: tuple with iType name and list of available instruments of current type for defined user token.
 521        """
 522        result = []
 523
 524        if iType in TKS_INSTRUMENTS:
 525            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 526
 527            # all instruments have the same body in API v2 requests:
 528            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 529            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 530            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 531
 532        return iType, result
 533
 534    def _IWrapper(self, kwargs):
 535        """
 536        Wrapper runs instrument's update method `_IUpdater()`.
 537        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 538        """
 539        return self._IUpdater(**kwargs)
 540
 541    def Listing(self) -> dict:
 542        """
 543        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 544
 545        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 546        """
 547        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 548        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 549
 550        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 551        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 552        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 553
 554        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 555        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 556        poolUpdater.close()  # close the thread pool
 557        poolUpdater.join()  # wait a moment until all data returns from threads
 558
 559        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 560        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 561        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 562
 563        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 564        for iType in iList.keys():
 565            for ticker in iList[iType]:
 566                iList[iType][ticker]["type"] = iType
 567
 568                if "minPriceIncrement" in iList[iType][ticker].keys():
 569                    iList[iType][ticker]["step"] = NanoToFloat(
 570                        iList[iType][ticker]["minPriceIncrement"]["units"],
 571                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 572                    )
 573
 574                else:
 575                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 576
 577        return iList
 578
 579    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 580        """
 581        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 582
 583        See also: `DumpInstruments()`, `Listing()`.
 584
 585        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 586                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 587        """
 588        if self.iListDumpFile is None or not self.iListDumpFile:
 589            uLogger.error("Output name of dump file must be defined!")
 590            raise Exception("Filename required")
 591
 592        if not self.iList or forceUpdate:
 593            self.iList = self.Listing()
 594
 595        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 596
 597        # Save as XLSX with separated sheets for every type of instruments:
 598        with pd.ExcelWriter(
 599                path=xlsxDumpFile,
 600                date_format=TKS_DATE_FORMAT,
 601                datetime_format=TKS_DATE_TIME_FORMAT,
 602                mode="w",
 603        ) as writer:
 604            for iType in TKS_INSTRUMENTS:
 605                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 606                df = df[sorted(df)]  # sorted by column names
 607                df = df.applymap(
 608                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 609                    na_action="ignore",
 610                )  # converting numbers from nano-type to float in every cell
 611                df.to_excel(
 612                    writer,
 613                    sheet_name=iType,
 614                    encoding="UTF-8",
 615                    freeze_panes=(1, 1),
 616                )  # saving as XLSX-file with freeze first row and column as headers
 617
 618        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 619
 620    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 621        """
 622        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 623        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 624
 625        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 626
 627        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 628                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 629        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 630        """
 631        if self.iListDumpFile is None or not self.iListDumpFile:
 632            uLogger.error("Output name of dump file must be defined!")
 633            raise Exception("Filename required")
 634
 635        if not self.iList or forceUpdate:
 636            self.iList = self.Listing()
 637
 638        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 639        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 640            fH.write(jsonDump)
 641
 642        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 643
 644        return jsonDump
 645
 646    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 647        """
 648        Show information about one instrument defined by json data and prints it in Markdown format.
 649
 650        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 651
 652        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
 653        :param show: if `True` then also printing information about instrument and its current price.
 654        :return: multilines text in Markdown format with information about one instrument.
 655        """
 656        splitLine = "|                                                             |                                                        |\n"
 657        infoText = ""
 658
 659        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 660            info = [
 661                "# Main information\n\n",
 662                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
 663                "| Parameters                                                  | Values                                                 |\n",
 664                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 665                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 666                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 667            ]
 668
 669            if "sector" in iJSON.keys() and iJSON["sector"]:
 670                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 671
 672            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
 673                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
 674
 675            info.extend([
 676                splitLine,
 677                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 678                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
 679            ])
 680
 681            if "isin" in iJSON.keys() and iJSON["isin"]:
 682                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 683
 684            if "classCode" in iJSON.keys():
 685                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 686
 687            info.extend([
 688                splitLine,
 689                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 690                splitLine,
 691                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 692                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 693                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 694            ])
 695
 696            if iJSON["figi"]:
 697                self._figi = iJSON["figi"]
 698                iJSON = iJSON | self.RequestTradingStatus()
 699
 700                info.extend([
 701                    splitLine,
 702                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 703                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 704                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 705                ])
 706
 707            info.append(splitLine)
 708
 709            if "type" in iJSON.keys() and iJSON["type"]:
 710                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 711
 712                if "shareType" in iJSON.keys() and iJSON["shareType"]:
 713                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
 714
 715            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 716                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 717
 718            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 719                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 720
 721            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 722                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 723
 724            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 725                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 726
 727            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 728                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 729
 730            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 731                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 732
 733            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 734                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 735
 736            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 737                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 738
 739            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 740                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 741
 742            if "currency" in iJSON.keys():
 743                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 744
 745            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 746                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 747
 748            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 749                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 750
 751            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 752                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 753
 754            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 755                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 756
 757            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 758                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 759
 760            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 761                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 762
 763            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 764                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 765
 766            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 767                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 768
 769            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 770                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 771
 772            iExt = None
 773            if iJSON["type"] == "Bonds":
 774                info.extend([
 775                    splitLine,
 776                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 777                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 778                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 779                        iJSON["nominal"]["currency"],
 780                    )),
 781                ])
 782
 783                if "floatingCouponFlag" in iJSON.keys():
 784                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 785
 786                if "amortizationFlag" in iJSON.keys():
 787                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 788
 789                info.append(splitLine)
 790
 791                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 792                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 793
 794                if iJSON["figi"]:
 795                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 796
 797                    info.extend([
 798                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 799                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 800                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 801                    ])
 802
 803                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 804                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 805                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 806                        iJSON["aciValue"]["currency"]
 807                    )))
 808
 809            if "currentPrice" in iJSON.keys():
 810                info.append(splitLine)
 811
 812                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 813                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 814
 815                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 816                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 817                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 818                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 819                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 820
 821                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 822                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 823
 824                info.extend([
 825                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 826                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 827                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 828                    )),
 829                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 830                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 831                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 832                    )),
 833                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 834                        "{:.2f}%{}".format(
 835                            iJSON["currentPrice"]["changes"],
 836                            " ({}{:.2f} {})".format(
 837                                "+" if bondChangesDelta > 0 else "",
 838                                bondChangesDelta,
 839                                aciCurrency
 840                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 841                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 842                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 843                                currency
 844                            ),
 845                        )
 846                    ),
 847                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 848                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 849                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 850                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 851                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 852                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 853                    )),
 854                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 855                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 856                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 857                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 858                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 859                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 860                    )),
 861                ])
 862
 863            if "lot" in iJSON.keys():
 864                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 865
 866            if "step" in iJSON.keys() and iJSON["step"] != 0:
 867                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 868
 869            # Add bond payment calendar:
 870            if iJSON["type"] == "Bonds":
 871                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 872                info.extend(["\n#", strCalendar])
 873
 874            infoText += "".join(info)
 875
 876            if show:
 877                uLogger.info("{}".format(infoText))
 878
 879            else:
 880                uLogger.debug("{}".format(infoText))
 881
 882            if self.infoFile is not None:
 883                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 884                    fH.write(infoText)
 885
 886                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 887
 888                if self.useHTMLReports:
 889                    htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html"
 890                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
 891                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText))
 892
 893                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
 894
 895        return infoText
 896
 897    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 898        """
 899        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 900
 901        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 902        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 903        :return: JSON formatted data with information about instrument.
 904        """
 905        tickerJSON = {}
 906        if self.moreDebug:
 907            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
 908
 909        if not self._ticker:
 910            uLogger.warning("self._ticker variable is not be empty!")
 911
 912        else:
 913            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 914                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
 915                raise Exception("Instrument not allowed")
 916
 917            if not self.iList:
 918                self.iList = self.Listing()
 919
 920            if self._ticker in self.iList["Shares"].keys():
 921                tickerJSON = self.iList["Shares"][self._ticker]
 922                if self.moreDebug:
 923                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
 924
 925            elif self._ticker in self.iList["Currencies"].keys():
 926                tickerJSON = self.iList["Currencies"][self._ticker]
 927                if self.moreDebug:
 928                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
 929
 930            elif self._ticker in self.iList["Bonds"].keys():
 931                tickerJSON = self.iList["Bonds"][self._ticker]
 932                if self.moreDebug:
 933                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
 934
 935            elif self._ticker in self.iList["Etfs"].keys():
 936                tickerJSON = self.iList["Etfs"][self._ticker]
 937                if self.moreDebug:
 938                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
 939
 940            elif self._ticker in self.iList["Futures"].keys():
 941                tickerJSON = self.iList["Futures"][self._ticker]
 942                if self.moreDebug:
 943                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
 944
 945        if tickerJSON:
 946            self._figi = tickerJSON["figi"]
 947
 948            if requestPrice:
 949                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 950
 951                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 952                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 953
 954                else:
 955                    tickerJSON["currentPrice"]["changes"] = 0
 956
 957            if show:
 958                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 959
 960        else:
 961            if show:
 962                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
 963
 964        return tickerJSON
 965
 966    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 967        """
 968        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
 969
 970        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
 971        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 972        :return: JSON formatted data with information about instrument.
 973        """
 974        figiJSON = {}
 975        if self.moreDebug:
 976            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
 977
 978        if not self._figi:
 979            uLogger.warning("self._figi variable is not be empty!")
 980
 981        else:
 982            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
 983                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
 984                raise Exception("Instrument not allowed")
 985
 986            if not self.iList:
 987                self.iList = self.Listing()
 988
 989            for item in self.iList["Shares"].keys():
 990                if self._figi == self.iList["Shares"][item]["figi"]:
 991                    figiJSON = self.iList["Shares"][item]
 992
 993                    if self.moreDebug:
 994                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
 995
 996                    break
 997
 998            if not figiJSON:
 999                for item in self.iList["Currencies"].keys():
1000                    if self._figi == self.iList["Currencies"][item]["figi"]:
1001                        figiJSON = self.iList["Currencies"][item]
1002
1003                        if self.moreDebug:
1004                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
1005
1006                        break
1007
1008            if not figiJSON:
1009                for item in self.iList["Bonds"].keys():
1010                    if self._figi == self.iList["Bonds"][item]["figi"]:
1011                        figiJSON = self.iList["Bonds"][item]
1012
1013                        if self.moreDebug:
1014                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
1015
1016                        break
1017
1018            if not figiJSON:
1019                for item in self.iList["Etfs"].keys():
1020                    if self._figi == self.iList["Etfs"][item]["figi"]:
1021                        figiJSON = self.iList["Etfs"][item]
1022
1023                        if self.moreDebug:
1024                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1025
1026                        break
1027
1028            if not figiJSON:
1029                for item in self.iList["Futures"].keys():
1030                    if self._figi == self.iList["Futures"][item]["figi"]:
1031                        figiJSON = self.iList["Futures"][item]
1032
1033                        if self.moreDebug:
1034                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1035
1036                        break
1037
1038        if figiJSON:
1039            self._figi = figiJSON["figi"]
1040            self._ticker = figiJSON["ticker"]
1041
1042            if requestPrice:
1043                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1044
1045                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1046                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1047
1048                else:
1049                    figiJSON["currentPrice"]["changes"] = 0
1050
1051            if show:
1052                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1053
1054        else:
1055            if show:
1056                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1057
1058        return figiJSON
1059
1060    def GetCurrentPrices(self, show: bool = True) -> dict:
1061        """
1062        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1063        `{"buy": [{"price": 1243.8, "quantity": 193},
1064                  {"price": 1244.0, "quantity": 168},
1065                  {"price": 1244.8, "quantity": 5},
1066                  {"price": 1245.0, "quantity": 61},
1067                  {"price": 1245.4, "quantity": 60}],
1068          "sell": [{"price": 1243.6, "quantity": 8},
1069                   {"price": 1242.6, "quantity": 10},
1070                   {"price": 1242.4, "quantity": 18},
1071                   {"price": 1242.2, "quantity": 50},
1072                   {"price": 1242.0, "quantity": 113}],
1073          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1074        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1075        - sell: list of dicts with Buyers prices,
1076            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1077            - quantity: volume value by current price in lots,
1078        - limitUp: current trade session limit price, maximum,
1079        - limitDown: current trade session limit price, minimum,
1080        - lastPrice: last deal price of the instrument,
1081        - closePrice: previous trade session close price of the instrument.
1082
1083        See also: `SearchByTicker()` and `SearchByFIGI()`.
1084        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1085        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1086
1087        :param show: if `True` then print DOM to log and console.
1088        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1089                 If an error occurred then returns an empty record:
1090                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1091        """
1092        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1093
1094        if self.depth < 1:
1095            uLogger.error("Depth of Market (DOM) must be >=1!")
1096            raise Exception("Incorrect value")
1097
1098        if not (self._ticker or self._figi):
1099            uLogger.error("self._ticker or self._figi variables must be defined!")
1100            raise Exception("Ticker or FIGI required")
1101
1102        if self._ticker and not self._figi:
1103            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1104            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1105
1106        if not self._ticker and self._figi:
1107            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1108            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1109
1110        if not self._figi:
1111            uLogger.error("FIGI is not defined!")
1112            raise Exception("Ticker or FIGI required")
1113
1114        else:
1115            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1116
1117            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1118            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1119            self.body = str({"figi": self._figi, "depth": self.depth})
1120            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1121
1122            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1123                # list of dicts with sellers orders:
1124                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1125
1126                # list of dicts with buyers orders:
1127                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1128
1129                # max price of instrument at this time:
1130                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1131
1132                # min price of instrument at this time:
1133                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1134
1135                # last price of deal with instrument:
1136                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1137
1138                # last close price of instrument:
1139                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1140
1141            else:
1142                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1143                uLogger.debug("Server response: {}".format(pricesResponse))
1144
1145            if show:
1146                if prices["buy"] or prices["sell"]:
1147                    info = [
1148                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1149                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1150                            self._ticker,
1151                            self._figi,
1152                            self.depth,
1153                        ),
1154                        "-" * 60, "\n",
1155                        "             Orders of Buyers | Orders of Sellers\n",
1156                        "-" * 60, "\n",
1157                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1158                        "-" * 60, "\n",
1159                    ]
1160
1161                    if not prices["buy"]:
1162                        info.append("                              | No orders!\n")
1163                        sumBuy = 0
1164
1165                    else:
1166                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1167                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1168                        for item in maxMinSorted:
1169                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1170
1171                    if not prices["sell"]:
1172                        info.append("No orders!                    |\n")
1173                        sumSell = 0
1174
1175                    else:
1176                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1177                        for item in prices["sell"]:
1178                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1179
1180                    info.extend([
1181                        "-" * 60, "\n",
1182                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1183                        "-" * 60, "\n",
1184                    ])
1185
1186                    infoText = "".join(info)
1187
1188                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1189
1190                else:
1191                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1192
1193        return prices
1194
1195    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1196        """
1197        This method get and show information about all available broker instruments for current user account.
1198        If `instrumentsFile` string is not empty then also save information to this file.
1199
1200        :param show: if `True` then print results to console, if `False` — print only to file.
1201        :return: multi-lines string with all available broker instruments
1202        """
1203        if not self.iList:
1204            self.iList = self.Listing()
1205
1206        info = [
1207            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1208            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1209        ]
1210
1211        # add instruments count by type:
1212        for iType in self.iList.keys():
1213            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1214
1215        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1216        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1217
1218        # generating info tables with all instruments by type:
1219        for iType in self.iList.keys():
1220            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1221
1222            for instrument in self.iList[iType].keys():
1223                iName = self.iList[iType][instrument]["name"]  # instrument's name
1224                if len(iName) > 57:
1225                    iName = "{}...".format(iName[:54])  # right trim for a long string
1226
1227                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1228                    self.iList[iType][instrument]["ticker"],
1229                    iName,
1230                    self.iList[iType][instrument]["figi"],
1231                    self.iList[iType][instrument]["currency"],
1232                    self.iList[iType][instrument]["lot"],
1233                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1234                ))
1235
1236        infoText = "".join(info)
1237
1238        if show:
1239            uLogger.info(infoText)
1240
1241        if self.instrumentsFile:
1242            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1243                fH.write(infoText)
1244
1245            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1246
1247            if self.useHTMLReports:
1248                htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html"
1249                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1250                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText))
1251
1252                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1253
1254        return infoText
1255
1256    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1257        """
1258        This method search and show information about instruments by part of its ticker, FIGI or name.
1259        If `searchResultsFile` string is not empty then also save information to this file.
1260
1261        :param pattern: string with part of ticker, FIGI or instrument's name.
1262        :param show: if `True` then print results to console, if `False` — return list of result only.
1263        :return: list of dictionaries with all found instruments.
1264        """
1265        if not self.iList:
1266            self.iList = self.Listing()
1267
1268        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contain only filtered instruments
1269        compiledPattern = re.compile(pattern, re.IGNORECASE)
1270
1271        for iType in self.iList:
1272            for instrument in self.iList[iType].values():
1273                searchResult = compiledPattern.search(" ".join(
1274                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1275                ))
1276
1277                if searchResult:
1278                    searchResults[iType][instrument["ticker"]] = instrument
1279
1280        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1281        info = [
1282            "# Search results\n\n",
1283            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1284            "* **Search pattern:** [{}]\n".format(pattern),
1285            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1286            '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n'
1287        ]
1288        infoShort = info[:]
1289
1290        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1291        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1292        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1293
1294        if resultsLen == 0:
1295            info.append("\nNo results\n")
1296            infoShort.append("\nNo results\n")
1297            uLogger.warning("No results. Try changing your search pattern.")
1298
1299        else:
1300            for iType in searchResults:
1301                iTypeValuesCount = len(searchResults[iType].values())
1302                if iTypeValuesCount > 0:
1303                    info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1304                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1305
1306                    for instrument in searchResults[iType].values():
1307                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1308                            instrument["type"],
1309                            instrument["ticker"],
1310                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1311                            instrument["figi"],
1312                        ))
1313
1314                    if iTypeValuesCount <= 5:
1315                        infoShort.extend(info[-iTypeValuesCount:])
1316
1317                    else:
1318                        infoShort.extend(info[-5:])
1319                        infoShort.append(skippedLine)
1320
1321        infoText = "".join(info)
1322        infoTextShort = "".join(infoShort)
1323
1324        if show:
1325            uLogger.info(infoTextShort)
1326            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1327
1328        if self.searchResultsFile:
1329            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1330                fH.write(infoText)
1331
1332            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1333
1334            if self.useHTMLReports:
1335                htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html"
1336                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1337                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText))
1338
1339                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1340
1341        return searchResults
1342
1343    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1344        """
1345        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1346
1347        :param instruments: list of strings with tickers or FIGIs.
1348        :return: list with unique instrument FIGIs only.
1349        """
1350        requestedInstruments = []
1351        for iName in instruments:
1352            if iName not in self.aliases.keys():
1353                if iName not in requestedInstruments:
1354                    requestedInstruments.append(iName)
1355
1356            else:
1357                if iName not in requestedInstruments:
1358                    if self.aliases[iName] not in requestedInstruments:
1359                        requestedInstruments.append(self.aliases[iName])
1360
1361        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1362
1363        onlyUniqueFIGIs = []
1364        for iName in requestedInstruments:
1365            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1366                continue
1367
1368            self._ticker = iName
1369            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1370
1371            if not iData:
1372                self._ticker = ""
1373                self._figi = iName
1374
1375                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1376
1377                if not iData:
1378                    self._figi = ""
1379                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1380
1381            if iData and iData["figi"] not in onlyUniqueFIGIs:
1382                onlyUniqueFIGIs.append(iData["figi"])
1383
1384        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1385
1386        return onlyUniqueFIGIs
1387
1388    def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]:
1389        """
1390        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1391
1392        See limits: https://tinkoff.github.io/investAPI/limits/
1393
1394        If `pricesFile` string is not empty then also save information to this file.
1395
1396        :param instruments: list of strings with tickers or FIGIs.
1397        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1398        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1399                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1400        """
1401        if instruments is None or not instruments:
1402            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1403            raise Exception("Ticker or FIGI required")
1404
1405        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1406
1407        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1408
1409        iList = []  # trying to get info and current prices about all unique instruments:
1410        for self._figi in onlyUniqueFIGIs:
1411            iData = self.SearchByFIGI(requestPrice=True)
1412            iList.append(iData)
1413
1414        self.ShowListOfPrices(iList, show)
1415
1416        return iList
1417
1418    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1419        """
1420        Show table contains current prices of given instruments.
1421
1422        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1423                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1424        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1425        :return: multilines text in Markdown format as a table contains current prices.
1426        """
1427        infoText = ""
1428
1429        if show or self.pricesFile:
1430            info = [
1431                "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1432                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1433                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1434            ]
1435
1436            for item in iList:
1437                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1438                    item["ticker"],
1439                    item["figi"],
1440                    item["type"],
1441                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1442                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1443                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1444                    "{} / {}".format(
1445                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1446                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1447                    ),
1448                    "{} / {}".format(
1449                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1450                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1451                    ),
1452                    item["currency"],
1453                ))
1454
1455            infoText = "".join(info)
1456
1457            if show:
1458                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1459
1460            if self.pricesFile:
1461                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1462                    fH.write(infoText)
1463
1464                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1465
1466                if self.useHTMLReports:
1467                    htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html"
1468                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1469                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText))
1470
1471                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1472
1473        return infoText
1474
1475    def RequestTradingStatus(self) -> dict:
1476        """
1477        Requesting trading status for the instrument defined by `figi` variable.
1478
1479        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1480
1481        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1482
1483        :return: dictionary with trading status attributes. Response example:
1484                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1485                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1486        """
1487        if self._figi is None or not self._figi:
1488            uLogger.error("Variable `figi` must be defined for using this method!")
1489            raise Exception("FIGI required")
1490
1491        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1492
1493        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1494        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1495        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1496
1497        if self.moreDebug:
1498            uLogger.debug("Records about current trading status successfully received")
1499
1500        return tradingStatus
1501
1502    def RequestPortfolio(self) -> dict:
1503        """
1504        Requesting actual user's portfolio for current `accountId`.
1505
1506        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1507
1508        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1509
1510        :return: dictionary with user's portfolio.
1511        """
1512        if self.accountId is None or not self.accountId:
1513            uLogger.error("Variable `accountId` must be defined for using this method!")
1514            raise Exception("Account ID required")
1515
1516        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1517
1518        self.body = str({"accountId": self.accountId})
1519        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1520        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1521
1522        if self.moreDebug:
1523            uLogger.debug("Records about user's portfolio successfully received")
1524
1525        return rawPortfolio
1526
1527    def RequestPositions(self) -> dict:
1528        """
1529        Requesting open positions by currencies and instruments for current `accountId`.
1530
1531        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1532
1533        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1534
1535        :return: dictionary with open positions by instruments.
1536        """
1537        if self.accountId is None or not self.accountId:
1538            uLogger.error("Variable `accountId` must be defined for using this method!")
1539            raise Exception("Account ID required")
1540
1541        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1542
1543        self.body = str({"accountId": self.accountId})
1544        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1545        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1546
1547        if self.moreDebug:
1548            uLogger.debug("Records about current open positions successfully received")
1549
1550        return rawPositions
1551
1552    def RequestPendingOrders(self) -> list:
1553        """
1554        Requesting current actual pending limit orders for current `accountId`.
1555
1556        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1557
1558        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1559
1560        :return: list of dictionaries with pending limit orders.
1561        """
1562        if self.accountId is None or not self.accountId:
1563            uLogger.error("Variable `accountId` must be defined for using this method!")
1564            raise Exception("Account ID required")
1565
1566        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1567
1568        self.body = str({"accountId": self.accountId})
1569        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1570        rawResponse = self.SendAPIRequest(ordersURL, reqType="POST")
1571
1572        if "orders" in rawResponse.keys():
1573            rawOrders = rawResponse["orders"]
1574            uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1575
1576        else:
1577            rawOrders = []
1578            uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse))
1579
1580        return rawOrders
1581
1582    def RequestStopOrders(self) -> list:
1583        """
1584        Requesting current actual stop orders for current `accountId`.
1585
1586        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1587
1588        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1589
1590        :return: list of dictionaries with stop orders.
1591        """
1592        if self.accountId is None or not self.accountId:
1593            uLogger.error("Variable `accountId` must be defined for using this method!")
1594            raise Exception("Account ID required")
1595
1596        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1597
1598        self.body = str({"accountId": self.accountId})
1599        stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1600        rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST")
1601
1602        if "stopOrders" in rawResponse.keys():
1603            rawStopOrders = rawResponse["stopOrders"]
1604            uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1605
1606        else:
1607            rawStopOrders = []
1608            uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse))
1609
1610        return rawStopOrders
1611
1612    def Overview(self, show: bool = False, details: str = "full") -> dict:
1613        """
1614        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1615        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1616        and `overviewBondsCalendarFile` are defined then also save information to file.
1617
1618        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1619        many requests about the state of the portfolio, and then, based on the received data, a large number
1620        of calculation and statistics are collected.
1621
1622        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1623        :param details: how detailed should the information be?
1624        - `full` — shows full available information about portfolio status (by default),
1625        - `positions` — shows only open positions,
1626        - `orders` — shows only sections of open limits and stop orders.
1627        - `digest` — show a short digest of the portfolio status,
1628        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1629        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1630        :return: dictionary with client's raw portfolio and some statistics.
1631        """
1632        if self.accountId is None or not self.accountId:
1633            uLogger.error("Variable `accountId` must be defined for using this method!")
1634            raise Exception("Account ID required")
1635
1636        view = {
1637            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1638                "headers": {},  # list of dictionaries, response headers without "positions" section
1639                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1640                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1641                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1642                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1643                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1644                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1645                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1646                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1647                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1648            },
1649            "stat": {  # --- some statistics calculated using "raw" sections:
1650                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1651                "availableRUB": 0.,  # available rubles (without other currencies)
1652                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1653                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1654                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1655                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1656                "sharesCostRUB": 0.,  # costs of all shares in RUB
1657                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1658                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1659                "futuresCostRUB": 0.,  # costs of all futures in RUB
1660                "Currencies": [],  # list of dictionaries of all currencies statistics
1661                "Shares": [],  # list of dictionaries of all shares statistics
1662                "Bonds": [],  # list of dictionaries of all bonds statistics
1663                "Etfs": [],  # list of dictionaries of all etfs statistics
1664                "Futures": [],  # list of dictionaries of all futures statistics
1665                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1666                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1667                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1668                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1669                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1670            },
1671            "analytics": {  # --- some analytics of portfolio:
1672                "distrByAssets": {},  # portfolio distribution by assets
1673                "distrByCompanies": {},  # portfolio distribution by companies
1674                "distrBySectors": {},  # portfolio distribution by sectors
1675                "distrByCurrencies": {},  # portfolio distribution by currencies
1676                "distrByCountries": {},  # portfolio distribution by countries
1677                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1678            }
1679        }
1680
1681        details = details.lower()
1682        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1683        if details not in availableDetails:
1684            details = "full"
1685            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1686
1687        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1688
1689        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1690        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1691        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1692        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1693
1694        # save response headers without "positions" section:
1695        for key in portfolioResponse.keys():
1696            if key != "positions":
1697                view["raw"]["headers"][key] = portfolioResponse[key]
1698
1699            else:
1700                continue
1701
1702        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1703        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1704        for item in portfolioResponse["positions"]:
1705            if item["instrumentType"] == "currency":
1706                self._figi = item["figi"]
1707                if not self._figi and item["ticker"]:
1708                    self._ticker = item["ticker"]
1709                    self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1710
1711                curr = self.SearchByFIGI(requestPrice=False)
1712
1713                # current price of currency in RUB:
1714                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1715                    "name": curr["name"],
1716                    "currentPrice": NanoToFloat(
1717                        item["currentPrice"]["units"],
1718                        item["currentPrice"]["nano"]
1719                    ),
1720                }
1721
1722                view["raw"]["Currencies"].append(item)
1723
1724            elif item["instrumentType"] == "share":
1725                view["raw"]["Shares"].append(item)
1726
1727            elif item["instrumentType"] == "bond":
1728                view["raw"]["Bonds"].append(item)
1729
1730            elif item["instrumentType"] == "etf":
1731                view["raw"]["Etfs"].append(item)
1732
1733            elif item["instrumentType"] == "futures":
1734                view["raw"]["Futures"].append(item)
1735
1736            else:
1737                continue
1738
1739        # how many volume of currencies (by ISO currency name) are blocked:
1740        for item in view["raw"]["positions"]["blocked"]:
1741            blocked = NanoToFloat(item["units"], item["nano"])
1742            if blocked > 0:
1743                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1744
1745        # how many volume of instruments (by FIGI) are blocked:
1746        for item in view["raw"]["positions"]["securities"]:
1747            blocked = int(item["blocked"])
1748            if blocked > 0:
1749                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1750
1751        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1752
1753        if "rub" in allBlocked.keys():
1754            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1755
1756        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1757        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1758        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1759        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1760        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1761        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1762        view["stat"]["portfolioCostRUB"] = sum([
1763            view["stat"]["allCurrenciesCostRUB"],
1764            view["stat"]["sharesCostRUB"],
1765            view["stat"]["bondsCostRUB"],
1766            view["stat"]["etfsCostRUB"],
1767            view["stat"]["futuresCostRUB"],
1768        ])
1769
1770        # --- calculating some portfolio statistics:
1771        byComp = {}  # distribution by companies
1772        bySect = {}  # distribution by sectors
1773        byCurr = {}  # distribution by currencies (include RUB)
1774        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1775        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1776
1777        for item in portfolioResponse["positions"]:
1778            self._figi = item["figi"]
1779            if not self._figi and item["ticker"]:
1780                self._ticker = item["ticker"]
1781                self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1782
1783            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1784
1785            if instrument:
1786                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1787                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1788
1789                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1790                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1791
1792                else:
1793                    blocked = 0
1794
1795                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1796                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1797                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1798                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1799                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1800                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1801                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1802                cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1803                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1804                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1805                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1806                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1807
1808                statData = {
1809                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1810                    "ticker": instrument["ticker"],  # ticker by FIGI
1811                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1812                    "volume": volume,  # available volume of instrument
1813                    "lots": lots,  # volume in lots of instrument
1814                    "direction": direction,  # direction of an instrument's position: short or long
1815                    "blocked": blocked,  # blocked volume of currency or instrument
1816                    "currentPrice": curPrice,  # current instrument's price in basic asset
1817                    "average": average,  # current average position price
1818                    "cost": cost,  # current cost of all volume of instrument in basic asset
1819                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1820                    "costRUB": costRUB,  # cost of instrument in ruble
1821                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1822                    "profit": profit,  # expected profit at current moment
1823                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1824                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1825                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1826                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1827                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1828                    "step": instrument["step"],  # minimum price increment
1829                }
1830
1831                # adding distribution by unique countries:
1832                if statData["country"] not in byCountry.keys():
1833                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1834
1835                else:
1836                    byCountry[statData["country"]]["cost"] += costRUB
1837                    byCountry[statData["country"]]["percent"] += percentCostRUB
1838
1839                if item["instrumentType"] != "currency":
1840                    # adding distribution by unique companies:
1841                    if statData["name"]:
1842                        if statData["name"] not in byComp.keys():
1843                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1844
1845                        else:
1846                            byComp[statData["name"]]["cost"] += costRUB
1847                            byComp[statData["name"]]["percent"] += percentCostRUB
1848
1849                    # adding distribution by unique sectors:
1850                    if statData["sector"] not in bySect.keys():
1851                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1852
1853                    else:
1854                        bySect[statData["sector"]]["cost"] += costRUB
1855                        bySect[statData["sector"]]["percent"] += percentCostRUB
1856
1857                # adding distribution by unique currencies:
1858                if currency not in byCurr.keys():
1859                    byCurr[currency] = {
1860                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1861                        "cost": costRUB,
1862                        "percent": percentCostRUB
1863                    }
1864
1865                else:
1866                    byCurr[currency]["cost"] += costRUB
1867                    byCurr[currency]["percent"] += percentCostRUB
1868
1869                # saving statistics for every instrument:
1870                if item["instrumentType"] == "currency":
1871                    view["stat"]["Currencies"].append(statData)
1872
1873                    # update dict with free funds for trading (total - blocked) by currencies
1874                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1875                    view["stat"]["funds"][currency] = {
1876                        "total": volume,
1877                        "totalCostRUB": costRUB,  # total volume cost in rubles
1878                        "free": volume - blocked,
1879                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1880                    }
1881
1882                elif item["instrumentType"] == "share":
1883                    view["stat"]["Shares"].append(statData)
1884
1885                elif item["instrumentType"] == "bond":
1886                    view["stat"]["Bonds"].append(statData)
1887
1888                elif item["instrumentType"] == "etf":
1889                    view["stat"]["Etfs"].append(statData)
1890
1891                elif item["instrumentType"] == "Futures":
1892                    view["stat"]["Futures"].append(statData)
1893
1894                else:
1895                    continue
1896
1897        # total changes in Russian Ruble:
1898        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1899        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1900        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1901        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1902        view["stat"]["funds"]["rub"] = {
1903            "total": view["stat"]["availableRUB"],
1904            "totalCostRUB": view["stat"]["availableRUB"],
1905            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1906            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1907        }
1908
1909        # --- pending limit orders sector data:
1910        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1911        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1912
1913        for item in view["raw"]["orders"]:
1914            self._figi = item["figi"]
1915
1916            if item["figi"] not in uniquePendingOrdersFIGIs:
1917                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1918
1919                uniquePendingOrdersFIGIs.append(item["figi"])
1920                uniquePendingOrders[item["figi"]] = instrument
1921
1922            else:
1923                instrument = uniquePendingOrders[item["figi"]]
1924
1925            if instrument:
1926                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1927                orderType = TKS_ORDER_TYPES[item["orderType"]]
1928                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1929                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1930
1931                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1932                if item["direction"] == "ORDER_DIRECTION_BUY":
1933                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1934
1935                else:
1936                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1937
1938                # requested price for order execution:
1939                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1940
1941                # necessary changes in percent to reach target from current price:
1942                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1943
1944                view["stat"]["orders"].append({
1945                    "orderID": item["orderId"],  # orderId number parameter of current order
1946                    "figi": item["figi"],  # FIGI identification
1947                    "ticker": instrument["ticker"],  # ticker name by FIGI
1948                    "lotsRequested": item["lotsRequested"],  # requested lots value
1949                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1950                    "currentPrice": lastPrice,  # current instrument's price for defined action
1951                    "targetPrice": target,  # requested price for order execution in base currency
1952                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1953                    "percentChanges": changes,  # changes in percent to target from current price
1954                    "currency": item["currency"],  # instrument's currency name
1955                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1956                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1957                    "status": orderState,  # order status from TKS_ORDER_STATES
1958                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1959                })
1960
1961        # --- stop orders sector data:
1962        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1963        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1964
1965        for item in view["raw"]["stopOrders"]:
1966            self._figi = item["figi"]
1967
1968            if item["figi"] not in uniqueStopOrdersFIGIs:
1969                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1970
1971                uniqueStopOrdersFIGIs.append(item["figi"])
1972                uniqueStopOrders[item["figi"]] = instrument
1973
1974            else:
1975                instrument = uniqueStopOrders[item["figi"]]
1976
1977            if instrument:
1978                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1979                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1980                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1981
1982                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1983                if "expirationTime" in item.keys():
1984                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1985                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1986
1987                else:
1988                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1989                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1990
1991                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1992                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1993                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1994
1995                else:
1996                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1997
1998                # requested price when stop-order executed:
1999                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
2000
2001                # price for limit-order, set up when stop-order executed:
2002                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
2003
2004                # necessary changes in percent to reach target from current price:
2005                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
2006
2007                view["stat"]["stopOrders"].append({
2008                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2009                    "figi": item["figi"],  # FIGI identification
2010                    "ticker": instrument["ticker"],  # ticker name by FIGI
2011                    "lotsRequested": item["lotsRequested"],  # requested lots value
2012                    "currentPrice": lastPrice,  # current instrument's price for defined action
2013                    "targetPrice": target,  # requested price for stop-order execution in base currency
2014                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2015                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2016                    "percentChanges": changes,  # changes in percent to target from current price
2017                    "currency": item["currency"],  # instrument's currency name
2018                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2019                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2020                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2021                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2022                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2023                })
2024
2025        # --- calculating data for analytics section:
2026        # portfolio distribution by assets:
2027        view["analytics"]["distrByAssets"] = {
2028            "Ruble": {
2029                "uniques": 1,
2030                "cost": view["stat"]["availableRUB"],
2031                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2032            },
2033            "Currencies": {
2034                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2035                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2036                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2037            },
2038            "Shares": {
2039                "uniques": len(view["stat"]["Shares"]),
2040                "cost": view["stat"]["sharesCostRUB"],
2041                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2042            },
2043            "Bonds": {
2044                "uniques": len(view["stat"]["Bonds"]),
2045                "cost": view["stat"]["bondsCostRUB"],
2046                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2047            },
2048            "Etfs": {
2049                "uniques": len(view["stat"]["Etfs"]),
2050                "cost": view["stat"]["etfsCostRUB"],
2051                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2052            },
2053            "Futures": {
2054                "uniques": len(view["stat"]["Futures"]),
2055                "cost": view["stat"]["futuresCostRUB"],
2056                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2057            },
2058        }
2059
2060        # portfolio distribution by companies:
2061        view["analytics"]["distrByCompanies"]["All money cash"] = {
2062            "ticker": "",
2063            "cost": view["stat"]["allCurrenciesCostRUB"],
2064            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2065        }
2066        view["analytics"]["distrByCompanies"].update(byComp)
2067
2068        # portfolio distribution by sectors:
2069        view["analytics"]["distrBySectors"]["All money cash"] = {
2070            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2071            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2072        }
2073        view["analytics"]["distrBySectors"].update(bySect)
2074
2075        # portfolio distribution by currencies:
2076        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2077            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2078
2079            if self.moreDebug:
2080                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2081
2082        view["analytics"]["distrByCurrencies"].update(byCurr)
2083        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2084        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2085
2086        # portfolio distribution by countries:
2087        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2088            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2089
2090            if self.moreDebug:
2091                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2092
2093        view["analytics"]["distrByCountries"].update(byCountry)
2094        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2095        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2096
2097        # --- Prepare text statistics overview in human-readable:
2098        if show:
2099            actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)
2100
2101            # Whatever the value `details`, header not changes:
2102            info = [
2103                "# Client's portfolio\n\n",
2104                "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2105                "* **Account ID:** [{}]\n".format(self.accountId),
2106            ]
2107
2108            if details in ["full", "positions", "digest"]:
2109                info.extend([
2110                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2111                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2112                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2113                        view["stat"]["totalChangesRUB"],
2114                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2115                        view["stat"]["totalChangesPercentRUB"],
2116                    ),
2117                ])
2118
2119            if details in ["full", "positions"]:
2120                info.extend([
2121                    "## Open positions\n\n",
2122                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2123                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2124                    "| **Ruble:**                  | {:>31} |          |              |              |                     |                              |\n".format(
2125                        "{:.2f} ({:.2f}) rub".format(
2126                            view["stat"]["availableRUB"],
2127                            view["stat"]["blockedRUB"],
2128                        )
2129                    )
2130                ])
2131
2132                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2133                    return [
2134                        "|                             |                                 |          |              |              |                     |                              |\n",
2135                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2136                            noTradeStr if noTradeStr else typeStr,
2137                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2138                        ),
2139                    ]
2140
2141                def _InfoStr(data: dict, isCurr: bool = False) -> str:
2142                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2143                        "{} [{}]".format(data["ticker"], data["figi"]),
2144                        "{:.2f} ({:.2f}) {}".format(
2145                            data["volume"],
2146                            data["blocked"],
2147                            data["currency"],
2148                        ) if isCurr else "{:.0f} ({:.0f})".format(
2149                            data["volume"],
2150                            data["blocked"],
2151                        ),
2152                        "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."),
2153                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2154                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2155                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2156                        "{}{:.2f} {} ({}{:.2f}%)".format(
2157                            "+" if data["profit"] > 0 else "",
2158                            data["profit"], data["baseCurrencyName"],
2159                            "+" if data["percentProfit"] > 0 else "",
2160                            data["percentProfit"],
2161                        ),
2162                    )
2163
2164                # --- Show currencies section:
2165                if view["stat"]["Currencies"]:
2166                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2167                    for item in view["stat"]["Currencies"]:
2168                        info.append(_InfoStr(item, isCurr=True))
2169
2170                else:
2171                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2172
2173                # --- Show shares section:
2174                if view["stat"]["Shares"]:
2175                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2176
2177                    for item in view["stat"]["Shares"]:
2178                        info.append(_InfoStr(item))
2179
2180                else:
2181                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2182
2183                # --- Show bonds section:
2184                if view["stat"]["Bonds"]:
2185                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2186
2187                    for item in view["stat"]["Bonds"]:
2188                        info.append(_InfoStr(item))
2189
2190                else:
2191                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2192
2193                # --- Show etfs section:
2194                if view["stat"]["Etfs"]:
2195                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2196
2197                    for item in view["stat"]["Etfs"]:
2198                        info.append(_InfoStr(item))
2199
2200                else:
2201                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2202
2203                # --- Show futures section:
2204                if view["stat"]["Futures"]:
2205                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2206
2207                    for item in view["stat"]["Futures"]:
2208                        info.append(_InfoStr(item))
2209
2210                else:
2211                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2212
2213            if details in ["full", "orders"]:
2214                # --- Show pending limit orders section:
2215                if view["stat"]["orders"]:
2216                    info.extend([
2217                        "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])),
2218                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2219                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2220                    ])
2221
2222                    for item in view["stat"]["orders"]:
2223                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2224                            "{} [{}]".format(item["ticker"], item["figi"]),
2225                            item["orderID"],
2226                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2227                            "{} {} ({}{:.2f}%)".format(
2228                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2229                                item["baseCurrencyName"],
2230                                "+" if item["percentChanges"] > 0 else "",
2231                                float(item["percentChanges"]),
2232                            ),
2233                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2234                            item["action"],
2235                            item["type"],
2236                            item["date"],
2237                        ))
2238
2239                else:
2240                    info.append("\n## Total pending limit-orders: [0]\n")
2241
2242                # --- Show stop orders section:
2243                if view["stat"]["stopOrders"]:
2244                    info.extend([
2245                        "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])),
2246                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2247                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2248                    ])
2249
2250                    for item in view["stat"]["stopOrders"]:
2251                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2252                            "{} [{}]".format(item["ticker"], item["figi"]),
2253                            item["orderID"],
2254                            item["lotsRequested"],
2255                            "{} {} ({}{:.2f}%)".format(
2256                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2257                                item["baseCurrencyName"],
2258                                "+" if item["percentChanges"] > 0 else "",
2259                                float(item["percentChanges"]),
2260                            ),
2261                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2262                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2263                            item["action"],
2264                            item["type"],
2265                            item["expType"],
2266                            item["createDate"],
2267                            item["expDate"],
2268                        ))
2269
2270                else:
2271                    info.append("\n## Total stop-orders: [0]\n")
2272
2273            if details in ["full", "analytics"]:
2274                # -- Show analytics section:
2275                if view["stat"]["portfolioCostRUB"] > 0:
2276                    info.extend([
2277                        "\n# Analytics\n\n"
2278                        "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2279                        "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2280                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2281                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2282                            view["stat"]["totalChangesRUB"],
2283                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2284                            view["stat"]["totalChangesPercentRUB"],
2285                        ),
2286                        "\n## Portfolio distribution by assets\n"
2287                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2288                        "|------------------------------------|---------|---------|--------------------|\n",
2289                    ])
2290
2291                    for key in view["analytics"]["distrByAssets"].keys():
2292                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2293                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2294                                key,
2295                                view["analytics"]["distrByAssets"][key]["uniques"],
2296                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2297                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2298                            ))
2299
2300                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2301
2302                    info.extend([
2303                        "\n## Portfolio distribution by companies\n"
2304                        "\n| Company                                      | Percent | Current cost       |\n",
2305                        aSepLine,
2306                    ])
2307
2308                    for company in view["analytics"]["distrByCompanies"].keys():
2309                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2310                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2311                                "{}{}".format(
2312                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2313                                    company,
2314                                ),
2315                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2316                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2317                            ))
2318
2319                    info.extend([
2320                        "\n## Portfolio distribution by sectors\n"
2321                        "\n| Sector                                       | Percent | Current cost       |\n",
2322                        aSepLine,
2323                    ])
2324
2325                    for sector in view["analytics"]["distrBySectors"].keys():
2326                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2327                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2328                                sector,
2329                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2330                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2331                            ))
2332
2333                    info.extend([
2334                        "\n## Portfolio distribution by currencies\n"
2335                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2336                        aSepLine,
2337                    ])
2338
2339                    for curr in view["analytics"]["distrByCurrencies"].keys():
2340                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2341                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2342                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2343                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2344                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2345                            ))
2346
2347                    info.extend([
2348                        "\n## Portfolio distribution by countries\n"
2349                        "\n| Assets by country                            | Percent | Current cost       |\n",
2350                        aSepLine,
2351                    ])
2352
2353                    for country in view["analytics"]["distrByCountries"].keys():
2354                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2355                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2356                                country,
2357                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2358                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2359                            ))
2360
2361            if details in ["full", "calendar"]:
2362                # -- Show bonds payment calendar section:
2363                if view["stat"]["Bonds"]:
2364                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2365                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2366                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2367
2368                else:
2369                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2370
2371            infoText = "".join(info)
2372
2373            uLogger.info(infoText)
2374
2375            if details == "full" and self.overviewFile:
2376                filename = self.overviewFile
2377
2378            elif details == "digest" and self.overviewDigestFile:
2379                filename = self.overviewDigestFile
2380
2381            elif details == "positions" and self.overviewPositionsFile:
2382                filename = self.overviewPositionsFile
2383
2384            elif details == "orders" and self.overviewOrdersFile:
2385                filename = self.overviewOrdersFile
2386
2387            elif details == "analytics" and self.overviewAnalyticsFile:
2388                filename = self.overviewAnalyticsFile
2389
2390            elif details == "calendar" and self.overviewBondsCalendarFile:
2391                filename = self.overviewBondsCalendarFile
2392
2393            else:
2394                filename = ""
2395
2396            if filename:
2397                with open(filename, "w", encoding="UTF-8") as fH:
2398                    fH.write(infoText)
2399
2400                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2401
2402                if self.useHTMLReports:
2403                    htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html"
2404                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2405                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText))
2406
2407                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2408
2409        return view
2410
2411    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2412        """
2413        Returns history operations between two given dates for current `accountId`.
2414        If `reportFile` string is not empty then also save human-readable report.
2415        Shows some statistical data of closed positions.
2416
2417        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2418        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2419        :param show: if `True` then also prints all records to the console.
2420        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2421        :return: original list of dictionaries with history of deals records from API ("operations" key):
2422                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2423                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2424        """
2425        if self.accountId is None or not self.accountId:
2426            uLogger.error("Variable `accountId` must be defined for using this method!")
2427            raise Exception("Account ID required")
2428
2429        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2430
2431        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2432
2433        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2434        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2435        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2436        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2437        customStat = {}  # custom statistics in additional to responseJSON
2438
2439        # --- output report in human-readable format:
2440        if show or self.reportFile:
2441            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2442            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2443            nextDay = ""
2444
2445            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2446
2447            if len(ops) > 0:
2448                customStat = {
2449                    "opsCount": 0,  # total operations count
2450                    "buyCount": 0,  # buy operations
2451                    "sellCount": 0,  # sell operations
2452                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2453                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2454                    "payIn": {"rub": 0.},  # Deposit brokerage account
2455                    "payOut": {"rub": 0.},  # Withdrawals
2456                    "divs": {"rub": 0.},  # Dividends income
2457                    "coupons": {"rub": 0.},  # Coupon's income
2458                    "brokerCom": {"rub": 0.},  # Service commissions
2459                    "serviceCom": {"rub": 0.},  # Service commissions
2460                    "marginCom": {"rub": 0.},  # Margin commissions
2461                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2462                }
2463
2464                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2465                for item in ops:
2466                    if item["state"] == "OPERATION_STATE_EXECUTED":
2467                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2468
2469                        # count buy operations:
2470                        if "_BUY" in item["operationType"]:
2471                            customStat["buyCount"] += 1
2472
2473                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2474                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2475
2476                            else:
2477                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2478
2479                        # count sell operations:
2480                        elif "_SELL" in item["operationType"]:
2481                            customStat["sellCount"] += 1
2482
2483                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2484                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2485
2486                            else:
2487                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2488
2489                        # count incoming operations:
2490                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2491                            if item["payment"]["currency"] in customStat["payIn"].keys():
2492                                customStat["payIn"][item["payment"]["currency"]] += payment
2493
2494                            else:
2495                                customStat["payIn"][item["payment"]["currency"]] = payment
2496
2497                        # count withdrawals operations:
2498                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2499                            if item["payment"]["currency"] in customStat["payOut"].keys():
2500                                customStat["payOut"][item["payment"]["currency"]] += payment
2501
2502                            else:
2503                                customStat["payOut"][item["payment"]["currency"]] = payment
2504
2505                        # count dividends income:
2506                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2507                            if item["payment"]["currency"] in customStat["divs"].keys():
2508                                customStat["divs"][item["payment"]["currency"]] += payment
2509
2510                            else:
2511                                customStat["divs"][item["payment"]["currency"]] = payment
2512
2513                        # count coupon's income:
2514                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2515                            if item["payment"]["currency"] in customStat["coupons"].keys():
2516                                customStat["coupons"][item["payment"]["currency"]] += payment
2517
2518                            else:
2519                                customStat["coupons"][item["payment"]["currency"]] = payment
2520
2521                        # count broker commissions:
2522                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2523                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2524                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2525
2526                            else:
2527                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2528
2529                        # count service commissions:
2530                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2531                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2532                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2533
2534                            else:
2535                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2536
2537                        # count margin commissions:
2538                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2539                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2540                                customStat["marginCom"][item["payment"]["currency"]] += payment
2541
2542                            else:
2543                                customStat["marginCom"][item["payment"]["currency"]] = payment
2544
2545                        # count withholding taxes:
2546                        elif "_TAX" in item["operationType"]:
2547                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2548                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2549
2550                            else:
2551                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2552
2553                        else:
2554                            continue
2555
2556                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2557
2558                # --- view "Actions" lines:
2559                info.extend([
2560                    "| Report sections            |                               |                              |                      |                        |\n",
2561                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2562                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2563                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2564                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2565                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2566                    ),
2567                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2568                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2569                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2570                    ),
2571                ])
2572
2573                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2574                for key in opsKeys:
2575                    if key == "rub":
2576                        continue
2577
2578                    info.extend([
2579                        "|                            |                               | {:<28} |                      |                        |\n".format(
2580                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2581                        ),
2582                        "|                            |                               | {:<28} |                      |                        |\n".format(
2583                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2584                        ),
2585                    ])
2586
2587                info.append(splitLine1)
2588
2589                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2590                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2591                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2592                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2593                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2594                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2595                    )
2596
2597                # --- view "Payments" lines:
2598                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2599                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2600
2601                for key in paymentsKeys:
2602                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2603
2604                info.append(splitLine1)
2605
2606                # --- view "Commissions and taxes" lines:
2607                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2608                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2609
2610                for key in comKeys:
2611                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2612
2613                info.extend([
2614                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2615                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2616                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2617                ])
2618
2619            else:
2620                info.append("Broker returned no operations during this period\n")
2621
2622            # --- view "Operations" section:
2623            for item in ops:
2624                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2625                    continue
2626
2627                else:
2628                    self._figi = item["figi"]
2629                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2630                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2631
2632                    # group of deals during one day:
2633                    if nextDay and item["date"].split("T")[0] != nextDay:
2634                        info.append(splitLine2)
2635                        nextDay = ""
2636
2637                    else:
2638                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2639
2640                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2641                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2642                        self._figi if self._figi else "—",
2643                        instrument["ticker"] if instrument else "—",
2644                        instrument["type"] if instrument else "—",
2645                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2646                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2647                        TKS_OPERATION_STATES[item["state"]],
2648                        TKS_OPERATION_TYPES[item["operationType"]],
2649                    ))
2650
2651            infoText = "".join(info)
2652
2653            if show:
2654                if self.moreDebug:
2655                    uLogger.debug("Records about history of a client's operations successfully received")
2656
2657                uLogger.info(infoText)
2658
2659            if self.reportFile:
2660                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2661                    fH.write(infoText)
2662
2663                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2664
2665                if self.useHTMLReports:
2666                    htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html"
2667                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2668                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText))
2669
2670                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2671
2672        return ops, customStat
2673
2674    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2675        """
2676        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2677
2678        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2679        Warning! Broker server used ISO UTC time by default.
2680
2681        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2682        Also, `historyFile` used to update history with `onlyMissing` parameter.
2683
2684        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2685
2686        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2687        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2688        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2689                         `"hour"`, `"day"`. Default: `"hour"`.
2690        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2691                            False by default. Warning! History appends only from last candle to current time
2692                            with always update last candle!
2693        :param csvSep: separator if csv-file is used, `,` by default.
2694        :param show: if `True` then also prints Pandas DataFrame to the console.
2695        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2696                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2697        """
2698        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2699        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2700        history = None  # empty pandas object for history
2701
2702        if interval not in TKS_CANDLE_INTERVALS.keys():
2703            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2704            raise Exception("Incorrect value")
2705
2706        if not (self._ticker or self._figi):
2707            uLogger.error("Ticker or FIGI must be defined!")
2708            raise Exception("Ticker or FIGI required")
2709
2710        if self._ticker and not self._figi:
2711            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2712            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2713
2714        if self._figi and not self._ticker:
2715            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2716            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2717
2718        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2719        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2720        if interval.lower() != "day":
2721            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2722
2723        delta = dtEnd - dtStart  # current UTC time minus last time in file
2724        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2725
2726        # calculate history length in candles:
2727        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2728        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2729            length += 1  # to avoid fraction time
2730
2731        # calculate data blocks count:
2732        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2733
2734        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2735        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2736        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2737        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2738        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2739
2740        tempOld = None  # pandas object for old history, if --only-missing key present
2741        lastTime = None  # datetime object of last old candle in file
2742
2743        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2744            uLogger.debug("--only-missing key present, add only last missing candles...")
2745            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2746
2747            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2748
2749            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2750            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2751            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2752            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2753
2754            # get last datetime object from last string in file or minus 1 delta if file is empty:
2755            if len(tempOld) > 0:
2756                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2757
2758            else:
2759                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2760
2761            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2762
2763        responseJSONs = []  # raw history blocks of data
2764
2765        blockEnd = dtEnd
2766        for item in range(blocks):
2767            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2768            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2769
2770            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2771                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2772            ))
2773
2774            if blockStart == blockEnd:
2775                uLogger.debug("Skipped this zero-length block...")
2776
2777            else:
2778                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2779                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2780                self.body = str({
2781                    "figi": self._figi,
2782                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2783                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2784                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2785                })
2786                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2787
2788                if "code" in responseJSON.keys():
2789                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2790
2791                else:
2792                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2793                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2794
2795                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2796
2797            blockEnd = blockStart
2798
2799        printCount = len(responseJSONs)  # candles to show in console
2800        if responseJSONs:
2801            tempHistory = pd.DataFrame(
2802                data={
2803                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2804                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2805                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2806                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2807                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2808                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2809                    "volume": [int(item["volume"]) for item in responseJSONs],
2810                },
2811                index=range(len(responseJSONs)),
2812                columns=["date", "time", "open", "high", "low", "close", "volume"],
2813            )
2814            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2815            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2816
2817            # append only newest candles to old history if --only-missing key present:
2818            if onlyMissing and tempOld is not None and lastTime is not None:
2819                index = 0  # find start index in tempHistory data:
2820
2821                for i, item in tempHistory.iterrows():
2822                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2823
2824                    if curTime == lastTime:
2825                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2826                        index = i
2827                        printCount = index + 1
2828                        break
2829
2830                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2831
2832            else:
2833                history = tempHistory  # if no `--only-missing` key then load full data from server
2834
2835            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2836
2837        if history is not None and not history.empty:
2838            if show:
2839                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2840                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2841                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2842                ))
2843
2844        else:
2845            uLogger.warning("Received an empty candles history!")
2846
2847        if self.historyFile is not None:
2848            if history is not None and not history.empty:
2849                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2850                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2851
2852            else:
2853                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2854
2855        else:
2856            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2857
2858        return history
2859
2860    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2861        """
2862        Load candles history from csv-file and return Pandas DataFrame object.
2863
2864        See also: `History()` and `ShowHistoryChart()` methods.
2865
2866        :param filePath: path to csv-file to open.
2867        """
2868        loadedHistory = None  # init candles data object
2869
2870        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2871
2872        if os.path.exists(filePath):
2873            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2874
2875            tfStr = self.priceModel.FormattedDelta(
2876                self.priceModel.timeframe,
2877                "{days} days {hours}h {minutes}m {seconds}s",
2878            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2879                self.priceModel.timeframe,
2880                "{hours}h {minutes}m {seconds}s",
2881            )
2882
2883            if loadedHistory is not None and not loadedHistory.empty:
2884                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2885                    len(loadedHistory),
2886                    tfStr,
2887                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2888                )
2889
2890            else:
2891                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2892
2893        else:
2894            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2895
2896        return loadedHistory
2897
2898    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2899        """
2900        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2901
2902        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2903        Default: `index.html` (both for interact and non-interact candlesticks chart).
2904
2905        See also: `History()` and `LoadHistory()` methods.
2906
2907        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2908        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2909                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2910                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2911                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2912        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2913                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2914        """
2915        if isinstance(candles, str):
2916            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2917            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2918
2919        elif isinstance(candles, pd.DataFrame):
2920            self.priceModel.prices = candles  # set candles chain from variable
2921            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2922
2923            if "datetime" not in candles.columns:
2924                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2925
2926        else:
2927            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2928            raise Exception("Incorrect value")
2929
2930        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2931
2932        if interact:
2933            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2934
2935            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2936
2937        else:
2938            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2939
2940            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2941
2942        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2943
2944    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2945        """
2946        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2947        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2948
2949        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2950
2951        :param operation: string "Buy" or "Sell".
2952        :param lots: volume, integer count of lots >= 1.
2953        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2954        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2955        :param expDate: string "Undefined" by default or local date in future,
2956                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2957        :return: JSON with response from broker server.
2958        """
2959        if self.accountId is None or not self.accountId:
2960            uLogger.error("Variable `accountId` must be defined for using this method!")
2961            raise Exception("Account ID required")
2962
2963        if operation is None or not operation or operation not in ("Buy", "Sell"):
2964            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2965            raise Exception("Incorrect value")
2966
2967        if lots is None or lots < 1:
2968            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2969            lots = 1
2970
2971        if tp is None or tp < 0:
2972            tp = 0
2973
2974        if sl is None or sl < 0:
2975            sl = 0
2976
2977        if expDate is None or not expDate:
2978            expDate = "Undefined"
2979
2980        if not (self._ticker or self._figi):
2981            uLogger.error("Ticker or FIGI must be defined!")
2982            raise Exception("Ticker or FIGI required")
2983
2984        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
2985        self._ticker = instrument["ticker"]
2986        self._figi = instrument["figi"]
2987
2988        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
2989
2990        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2991        self.body = str({
2992            "figi": self._figi,
2993            "quantity": str(lots),
2994            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2995            "accountId": str(self.accountId),
2996            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2997        })
2998        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2999
3000        if "orderId" in response.keys():
3001            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
3002                operation, response["orderId"],
3003                self._ticker, self._figi, lots,
3004                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
3005                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
3006                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
3007            ))
3008
3009            if tp > 0:
3010                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
3011
3012            if sl > 0:
3013                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3014
3015        else:
3016            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
3017
3018        return response
3019
3020    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3021        """
3022        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3023        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3024
3025        See also: `Order()` and `Trade()` docstrings.
3026
3027        :param lots: volume, integer count of lots >= 1.
3028        :param tp: float > 0, take profit price of stop-order.
3029        :param sl: float > 0, stop loss price of stop-order.
3030        :param expDate: it's a local date in future.
3031                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3032        :return: JSON with response from broker server.
3033        """
3034        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
3035
3036    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3037        """
3038        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3039        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3040
3041        See also: `Order()` and `Trade()` docstrings.
3042
3043        :param lots: volume, integer count of lots >= 1.
3044        :param tp: float > 0, take profit price of stop-order.
3045        :param sl: float > 0, stop loss price of stop-order.
3046        :param expDate: it's a local date in the future.
3047                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3048        :return: JSON with response from broker server.
3049        """
3050        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
3051
3052    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3053        """
3054        Close position of given instruments.
3055
3056        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3057        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3058                         This avoids unnecessary downloading data from the server.
3059        """
3060        if instruments is None or not instruments:
3061            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3062            raise Exception("Ticker or FIGI required")
3063
3064        if isinstance(instruments, str):
3065            instruments = [instruments]
3066
3067        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3068        if uniqueInstruments:
3069            if portfolio is None or not portfolio:
3070                portfolio = self.Overview(show=False)
3071
3072            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3073            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3074
3075            for self._figi in uniqueInstruments:
3076                if self._figi not in allOpened:
3077                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3078                    continue
3079
3080                # search open trade info about instrument by ticker:
3081                instrument = {}
3082                for iType in TKS_INSTRUMENTS:
3083                    if instrument:
3084                        break
3085
3086                    for item in portfolio["stat"][iType]:
3087                        if item["figi"] == self._figi:
3088                            instrument = item
3089                            break
3090
3091                if instrument:
3092                    self._ticker = instrument["ticker"]
3093                    self._figi = instrument["figi"]
3094
3095                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3096                        self._ticker,
3097                        self._figi,
3098                        int(instrument["volume"]),
3099                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3100                    ))
3101
3102                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3103
3104                    if tradeLots > 0:
3105                        if instrument["blocked"] > 0:
3106                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3107                                instrument["blocked"],
3108                                self._ticker,
3109                                tradeLots,
3110                            ))
3111
3112                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3113                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3114
3115                    else:
3116                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
3117
3118    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3119        """
3120        Close all positions of given instruments with defined type.
3121
3122        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3123        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3124                         This avoids unnecessary downloading data from the server.
3125        """
3126        if iType not in TKS_INSTRUMENTS:
3127            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3128
3129        else:
3130            if portfolio is None or not portfolio:
3131                portfolio = self.Overview(show=False)
3132
3133            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3134            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3135
3136            if tickers and portfolio:
3137                self.CloseTrades(tickers, portfolio)
3138
3139            else:
3140                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3141
3142    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3143        """
3144        Universal method to create market or limit orders with all available parameters for current `accountId`.
3145        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3146
3147        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3148        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3149
3150        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3151        then broker immediately open market order as you can do simple --buy or --sell operations!
3152
3153        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3154        When current price will go up or down to target price value then broker opens a limit order.
3155        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3156
3157        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3158
3159        :param operation: string "Buy" or "Sell".
3160        :param orderType: string "Limit" or "Stop".
3161        :param lots: volume, integer count of lots >= 1.
3162        :param targetPrice: target price > 0. This is open trade price for limit order.
3163        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3164                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3165        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3166                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3167                         Stop loss order always executed by market price.
3168        :param expDate: string "Undefined" by default or local date in future.
3169                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3170                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3171                        A limit order has no expiration date, it lasts until the end of the trading day.
3172        :return: JSON with response from broker server.
3173        """
3174        if self.accountId is None or not self.accountId:
3175            uLogger.error("Variable `accountId` must be defined for using this method!")
3176            raise Exception("Account ID required")
3177
3178        if operation is None or not operation or operation not in ("Buy", "Sell"):
3179            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3180            raise Exception("Incorrect value")
3181
3182        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3183            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3184            raise Exception("Incorrect value")
3185
3186        if lots is None or lots < 1:
3187            uLogger.error("You must define trade volume > 0: integer count of lots!")
3188            raise Exception("Incorrect value")
3189
3190        if targetPrice is None or targetPrice <= 0:
3191            uLogger.error("Target price for limit-order must be greater than 0!")
3192            raise Exception("Incorrect value")
3193
3194        if limitPrice is None or limitPrice <= 0:
3195            limitPrice = targetPrice
3196
3197        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3198            stopType = "Limit"
3199
3200        if expDate is None or not expDate:
3201            expDate = "Undefined"
3202
3203        if not (self._ticker or self._figi):
3204            uLogger.error("Tocker or FIGI must be defined!")
3205            raise Exception("Ticker or FIGI required")
3206
3207        response = {}
3208        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3209        self._ticker = instrument["ticker"]
3210        self._figi = instrument["figi"]
3211
3212        if orderType == "Limit":
3213            uLogger.debug(
3214                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3215                    self._ticker, self._figi,
3216                    operation, lots, targetPrice, instrument["currency"],
3217                ))
3218
3219            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3220            self.body = str({
3221                "figi": self._figi,
3222                "quantity": str(lots),
3223                "price": FloatToNano(targetPrice),
3224                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3225                "accountId": str(self.accountId),
3226                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3227            })
3228            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3229
3230            if "orderId" in response.keys():
3231                uLogger.info(
3232                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format(
3233                        response["orderId"], self._ticker, self._figi, operation, lots,
3234                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3235                    ))
3236
3237                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3238                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3239                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3240                            targetPrice, instrument["currency"],
3241                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3242                        ))
3243
3244                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3245                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3246                            targetPrice, instrument["currency"],
3247                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3248                        ))
3249
3250            else:
3251                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3252
3253        if orderType == "Stop":
3254            uLogger.debug(
3255                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3256                    self._ticker, self._figi,
3257                    operation, lots,
3258                    targetPrice, instrument["currency"],
3259                    limitPrice, instrument["currency"],
3260                    stopType, expDate,
3261                ))
3262
3263            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3264            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3265            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3266
3267            body = {
3268                "figi": self._figi,
3269                "quantity": str(lots),
3270                "price": FloatToNano(limitPrice),
3271                "stopPrice": FloatToNano(targetPrice),
3272                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3273                "accountId": str(self.accountId),
3274                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3275                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3276            }
3277
3278            if expDateUTC:
3279                body["expireDate"] = expDateUTC
3280
3281            self.body = str(body)
3282            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3283
3284            if "stopOrderId" in response.keys():
3285                uLogger.info(
3286                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format(
3287                        response["stopOrderId"], self._ticker, self._figi, operation, lots,
3288                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3289                        "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"],
3290                        TKS_STOP_ORDER_TYPES[stopOrderType],
3291                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3292                    ))
3293
3294                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3295                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3296                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3297                            targetPrice, instrument["currency"],
3298                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3299                        ))
3300
3301                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3302                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3303                            targetPrice, instrument["currency"],
3304                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3305                        ))
3306
3307            else:
3308                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3309
3310        return response
3311
3312    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3313        """
3314        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3315        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3316        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3317        See also: `Order()` docstring.
3318
3319        :param lots: volume, integer count of lots >= 1.
3320        :param targetPrice: target price > 0. This is open trade price for limit order.
3321        :return: JSON with response from broker server.
3322        """
3323        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3324
3325    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3326        """
3327        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3328        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3329        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3330        target price value then broker opens a limit order. See also: `Order()` docstring.
3331
3332        :param lots: volume, integer count of lots >= 1.
3333        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3334        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3335                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3336        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3337                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3338        :param expDate: string "Undefined" by default or local date in future.
3339                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3340                        This date is converting to UTC format for server.
3341        :return: JSON with response from broker server.
3342        """
3343        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3344
3345    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3346        """
3347        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3348        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3349        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3350        See also: `Order()` docstring.
3351
3352        :param lots: volume, integer count of lots >= 1.
3353        :param targetPrice: target price > 0. This is open trade price for limit order.
3354        :return: JSON with response from broker server.
3355        """
3356        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3357
3358    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3359        """
3360        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3361        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3362        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3363        target price value then broker opens a limit order. See also: `Order()` docstring.
3364
3365        :param lots: volume, integer count of lots >= 1.
3366        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3367        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3368                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3369        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3370                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3371        :param expDate: string "Undefined" by default or local date in future.
3372                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3373                        This date is converting to UTC format for server.
3374        :return: JSON with response from broker server.
3375        """
3376        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3377
3378    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3379        """
3380        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3381
3382        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3383        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3384                             This avoids unnecessary downloading data from the server.
3385        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3386        """
3387        if self.accountId is None or not self.accountId:
3388            uLogger.error("Variable `accountId` must be defined for using this method!")
3389            raise Exception("Account ID required")
3390
3391        if orderIDs:
3392            if allOrdersIDs is None:
3393                rawOrders = self.RequestPendingOrders()
3394                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3395
3396            if allStopOrdersIDs is None:
3397                rawStopOrders = self.RequestStopOrders()
3398                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3399
3400            for orderID in orderIDs:
3401                idInPendingOrders = orderID in allOrdersIDs
3402                idInStopOrders = orderID in allStopOrdersIDs
3403
3404                if not (idInPendingOrders or idInStopOrders):
3405                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3406                    continue
3407
3408                else:
3409                    if idInPendingOrders:
3410                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3411
3412                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3413                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3414                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3415                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3416
3417                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3418                            if self.moreDebug:
3419                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3420
3421                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3422
3423                        else:
3424                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3425
3426                    elif idInStopOrders:
3427                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3428
3429                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3430                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3431                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3432                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3433
3434                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3435                            if self.moreDebug:
3436                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3437
3438                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3439
3440                        else:
3441                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3442
3443                    else:
3444                        continue
3445
3446    def CloseAllOrders(self) -> None:
3447        """
3448        Gets a list of open pending and stop orders and cancel it all.
3449        """
3450        rawOrders = self.RequestPendingOrders()
3451        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3452        lenOrders = len(allOrdersIDs)
3453
3454        rawStopOrders = self.RequestStopOrders()
3455        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3456        lenSOrders = len(allStopOrdersIDs)
3457
3458        if lenOrders > 0 or lenSOrders > 0:
3459            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3460
3461            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3462
3463        else:
3464            uLogger.info("Orders not found, nothing to cancel.")
3465
3466    def CloseAll(self, *args) -> None:
3467        """
3468        Close all available (not blocked) opened trades and orders.
3469
3470        Also, you can select one or more keywords case-insensitive:
3471        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3472
3473        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3474        """
3475        overview = self.Overview(show=False)  # get all open trades info
3476
3477        if len(args) == 0:
3478            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3479            self.CloseAllOrders()  # close all pending and stop orders
3480
3481            for iType in TKS_INSTRUMENTS:
3482                if iType != "Currencies":
3483                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3484
3485        else:
3486            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3487            lowerArgs = [x.lower() for x in args]
3488
3489            if "orders" in lowerArgs:
3490                self.CloseAllOrders()  # close all pending and stop orders
3491
3492            for iType in TKS_INSTRUMENTS:
3493                if iType.lower() in lowerArgs and iType != "Currencies":
3494                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3495
3496    def CloseAllByTicker(self, instrument: str) -> None:
3497        """
3498        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3499
3500        This method searches opened trade and orders of instrument throw all portfolio and then use
3501        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3502
3503        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3504
3505        :param instrument: string with ticker.
3506        """
3507        if instrument is None or not instrument:
3508            uLogger.error("Ticker name must be defined for using this method!")
3509            raise Exception("Ticker required")
3510
3511        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3512
3513        self._ticker = instrument  # try to set instrument as ticker
3514        self._figi = ""
3515
3516        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3517        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3518
3519        if limitAll and self.IsInLimitOrders(portfolio=overview):
3520            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3521            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3522
3523        if stopAll and self.IsInStopOrders(portfolio=overview):
3524            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3525            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3526
3527        if self.IsInPortfolio(portfolio=overview):
3528            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3529            self.CloseTrades(instruments=[instrument], portfolio=overview)
3530
3531    def CloseAllByFIGI(self, instrument: str) -> None:
3532        """
3533        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3534
3535        This method searches opened trade and orders of instrument throw all portfolio and then use
3536        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3537
3538        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3539
3540        :param instrument: string with FIGI id.
3541        """
3542        if instrument is None or not instrument:
3543            uLogger.error("FIGI id must be defined for using this method!")
3544            raise Exception("FIGI required")
3545
3546        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3547
3548        self._ticker = ""
3549        self._figi = instrument  # try to set instrument as FIGI id
3550
3551        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3552        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3553
3554        if limitAll and self.IsInLimitOrders(portfolio=overview):
3555            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3556            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3557
3558        if stopAll and self.IsInStopOrders(portfolio=overview):
3559            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3560            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3561
3562        if self.IsInPortfolio(portfolio=overview):
3563            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3564            self.CloseTrades(instruments=[instrument], portfolio=overview)
3565
3566    @staticmethod
3567    def ParseOrderParameters(operation, **inputParameters):
3568        """
3569        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3570
3571        :param operation: string "Buy" or "Sell".
3572        :param inputParameters: this is dict of strings that looks like this
3573               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3574               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3575               "prices" key: one or more prices to open limit-orders
3576               Counts of values in lots and prices lists must be equals!
3577        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3578        """
3579        # TODO: update order grid work with api v2
3580        pass
3581        # uLogger.debug("Input parameters: {}".format(inputParameters))
3582        #
3583        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3584        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3585        #     raise Exception("Incorrect value")
3586        #
3587        # if "l" in inputParameters.keys():
3588        #     inputParameters["lots"] = inputParameters.pop("l")
3589        #
3590        # if "p" in inputParameters.keys():
3591        #     inputParameters["prices"] = inputParameters.pop("p")
3592        #
3593        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3594        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3595        #     raise Exception("Incorrect value")
3596        #
3597        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3598        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3599        #
3600        # if len(lots) != len(prices):
3601        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3602        #     raise Exception("Incorrect value")
3603        #
3604        # uLogger.debug("Extracted parameters for orders:")
3605        # uLogger.debug("lots = {}".format(lots))
3606        # uLogger.debug("prices = {}".format(prices))
3607        #
3608        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3609        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3610        # uLogger.debug("Order parameters: {}".format(result))
3611        #
3612        # return result
3613
3614    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3615        """
3616        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3617
3618        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3619        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3620        """
3621        result = False
3622        msg = "Instrument not defined!"
3623
3624        if portfolio is None or not portfolio:
3625            portfolio = self.Overview(show=False)
3626
3627        if self._ticker:
3628            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3629            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3630
3631            for iType in TKS_INSTRUMENTS:
3632                for instrument in portfolio["stat"][iType]:
3633                    if instrument["ticker"] == self._ticker:
3634                        result = True
3635                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3636                        break
3637
3638        elif self._figi:
3639            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3640            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3641
3642            for iType in TKS_INSTRUMENTS:
3643                for instrument in portfolio["stat"][iType]:
3644                    if instrument["figi"] == self._figi:
3645                        result = True
3646                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3647                        break
3648
3649        else:
3650            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3651
3652        uLogger.debug(msg)
3653
3654        return result
3655
3656    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3657        """
3658        Returns instrument from the user's portfolio if it presents there.
3659        Instrument must be defined by `ticker` (highly priority) or `figi`.
3660
3661        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3662        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3663        """
3664        result = None
3665        msg = "Instrument not defined!"
3666
3667        if portfolio is None or not portfolio:
3668            portfolio = self.Overview(show=False)
3669
3670        if self._ticker:
3671            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3672            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3673
3674            for iType in TKS_INSTRUMENTS:
3675                for instrument in portfolio["stat"][iType]:
3676                    if instrument["ticker"] == self._ticker:
3677                        result = instrument
3678                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3679                        break
3680
3681        elif self._figi:
3682            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3683            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3684
3685            for iType in TKS_INSTRUMENTS:
3686                for instrument in portfolio["stat"][iType]:
3687                    if instrument["figi"] == self._figi:
3688                        result = instrument
3689                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3690                        break
3691
3692        else:
3693            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3694
3695        uLogger.debug(msg)
3696
3697        return result
3698
3699    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3700        """
3701        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3702
3703        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3704
3705        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3706        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3707        """
3708        result = False
3709        msg = "Instrument not defined!"
3710
3711        if portfolio is None or not portfolio:
3712            portfolio = self.Overview(show=False)
3713
3714        if self._ticker:
3715            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3716            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3717
3718            for instrument in portfolio["stat"]["orders"]:
3719                if instrument["ticker"] == self._ticker:
3720                    result = True
3721                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3722                    break
3723
3724        elif self._figi:
3725            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3726            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3727
3728            for instrument in portfolio["stat"]["orders"]:
3729                if instrument["figi"] == self._figi:
3730                    result = True
3731                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3732                    break
3733
3734        else:
3735            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3736
3737        uLogger.debug(msg)
3738
3739        return result
3740
3741    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3742        """
3743        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3744        Instrument must be defined by `ticker` (highly priority) or `figi`.
3745
3746        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3747
3748        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3749        :return: list with `orderID`s of limit orders.
3750        """
3751        result = []
3752        msg = "Instrument not defined!"
3753
3754        if portfolio is None or not portfolio:
3755            portfolio = self.Overview(show=False)
3756
3757        if self._ticker:
3758            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3759            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3760
3761            for instrument in portfolio["stat"]["orders"]:
3762                if instrument["ticker"] == self._ticker:
3763                    result.append(instrument["orderID"])
3764
3765            if result:
3766                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3767
3768        elif self._figi:
3769            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3770            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3771
3772            for instrument in portfolio["stat"]["orders"]:
3773                if instrument["figi"] == self._figi:
3774                    result.append(instrument["orderID"])
3775
3776            if result:
3777                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3778
3779        else:
3780            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3781
3782        uLogger.debug(msg)
3783
3784        return result
3785
3786    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3787        """
3788        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3789
3790        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3791
3792        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3793        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3794        """
3795        result = False
3796        msg = "Instrument not defined!"
3797
3798        if portfolio is None or not portfolio:
3799            portfolio = self.Overview(show=False)
3800
3801        if self._ticker:
3802            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3803            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3804
3805            for instrument in portfolio["stat"]["stopOrders"]:
3806                if instrument["ticker"] == self._ticker:
3807                    result = True
3808                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3809                    break
3810
3811        elif self._figi:
3812            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3813            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3814
3815            for instrument in portfolio["stat"]["stopOrders"]:
3816                if instrument["figi"] == self._figi:
3817                    result = True
3818                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3819                    break
3820
3821        else:
3822            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3823
3824        uLogger.debug(msg)
3825
3826        return result
3827
3828    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3829        """
3830        Returns list with all `orderID`s of opened stop orders for the instrument.
3831        Instrument must be defined by `ticker` (highly priority) or `figi`.
3832
3833        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3834
3835        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3836        :return: list with `orderID`s of stop orders.
3837        """
3838        result = []
3839        msg = "Instrument not defined!"
3840
3841        if portfolio is None or not portfolio:
3842            portfolio = self.Overview(show=False)
3843
3844        if self._ticker:
3845            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3846            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3847
3848            for instrument in portfolio["stat"]["stopOrders"]:
3849                if instrument["ticker"] == self._ticker:
3850                    result.append(instrument["orderID"])
3851
3852            if result:
3853                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3854
3855        elif self._figi:
3856            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3857            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3858
3859            for instrument in portfolio["stat"]["stopOrders"]:
3860                if instrument["figi"] == self._figi:
3861                    result.append(instrument["orderID"])
3862
3863            if result:
3864                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3865
3866        else:
3867            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3868
3869        uLogger.debug(msg)
3870
3871        return result
3872
3873    def RequestLimits(self) -> dict:
3874        """
3875        Method for obtaining the available funds for withdrawal for current `accountId`.
3876
3877        See also:
3878        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3879        - `OverviewLimits()` method
3880
3881        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3882                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3883                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3884                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3885        """
3886        if self.accountId is None or not self.accountId:
3887            uLogger.error("Variable `accountId` must be defined for using this method!")
3888            raise Exception("Account ID required")
3889
3890        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3891
3892        self.body = str({"accountId": self.accountId})
3893        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3894        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3895
3896        if self.moreDebug:
3897            uLogger.debug("Records about available funds for withdrawal successfully received")
3898
3899        return rawLimits
3900
3901    def OverviewLimits(self, show: bool = False) -> dict:
3902        """
3903        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3904
3905        See also: `RequestLimits()`.
3906
3907        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3908        :return: dict with raw parsed data from server and some calculated statistics about it.
3909        """
3910        if self.accountId is None or not self.accountId:
3911            uLogger.error("Variable `accountId` must be defined for using this method!")
3912            raise Exception("Account ID required")
3913
3914        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3915
3916        view = {
3917            "rawLimits": rawLimits,
3918            "limits": {  # parsed data for every currency:
3919                "money": {  # this is an array of portfolio currency positions
3920                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3921                },
3922                "blocked": {  # this is an array of blocked currency
3923                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3924                },
3925                "blockedGuarantee": {  # this is locked money under collateral for futures
3926                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3927                },
3928            },
3929        }
3930
3931        # --- Prepare text table with limits in human-readable format:
3932        if show:
3933            info = [
3934                "# Withdrawal limits\n\n",
3935                "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3936                "* **Account ID:** [{}]\n".format(self.accountId),
3937            ]
3938
3939            if view["limits"]["money"]:
3940                info.extend([
3941                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3942                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3943                ])
3944
3945            else:
3946                info.append("\nNo withdrawal limits\n")
3947
3948            for curr in view["limits"]["money"].keys():
3949                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3950                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3951                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3952
3953                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3954                    "[{}]".format(curr),
3955                    "{:.2f}".format(view["limits"]["money"][curr]),
3956                    "{:.2f}".format(availableMoney),
3957                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3958                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3959                )
3960
3961                if curr == "rub":
3962                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3963
3964                else:
3965                    info.append(infoStr)
3966
3967            infoText = "".join(info)
3968
3969            uLogger.info(infoText)
3970
3971            if self.withdrawalLimitsFile:
3972                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3973                    fH.write(infoText)
3974
3975                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3976
3977                if self.useHTMLReports:
3978                    htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html"
3979                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
3980                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText))
3981
3982                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
3983
3984        return view
3985
3986    def RequestAccounts(self) -> dict:
3987        """
3988        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3989
3990        See also:
3991        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3992        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3993        - `OverviewUserInfo()` method
3994
3995        :return: dict with raw data from server that contains accounts info. Example of dict:
3996                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3997                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3998                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3999                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
4000        """
4001        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
4002
4003        self.body = str({})
4004        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
4005        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
4006
4007        if self.moreDebug:
4008            uLogger.debug("Records about available accounts successfully received")
4009
4010        return rawAccounts
4011
4012    def RequestUserInfo(self) -> dict:
4013        """
4014        Method for requesting common user's information.
4015
4016        See also:
4017        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
4018        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
4019        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
4020        - `OverviewUserInfo()` method
4021
4022        :return: dict with raw data from server that contains user's information. Example of dict:
4023                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
4024                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
4025        """
4026        uLogger.debug("Requesting common user's information. Wait, please...")
4027
4028        self.body = str({})
4029        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
4030        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
4031
4032        if self.moreDebug:
4033            uLogger.debug("Records about current user successfully received")
4034
4035        return rawUserInfo
4036
4037    def RequestMarginStatus(self, accountId: str = None) -> dict:
4038        """
4039        Method for requesting margin calculation for defined account ID.
4040
4041        See also:
4042        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
4043        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
4044        - `OverviewUserInfo()` method
4045
4046        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
4047        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
4048                 Example of responses:
4049                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
4050                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
4051                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
4052                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
4053                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
4054                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
4055        """
4056        if accountId is None or not accountId:
4057            if self.accountId is None or not self.accountId:
4058                uLogger.error("Variable `accountId` must be defined for using this method!")
4059                raise Exception("Account ID required")
4060
4061            else:
4062                accountId = self.accountId  # use `self.accountId` (main ID) by default
4063
4064        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
4065
4066        self.body = str({"accountId": accountId})
4067        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
4068        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
4069
4070        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
4071            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
4072            rawMargin = {}
4073
4074        else:
4075            if self.moreDebug:
4076                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
4077
4078        return rawMargin
4079
4080    def RequestTariffLimits(self) -> dict:
4081        """
4082        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4083
4084        See also:
4085        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4086        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4087        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4088        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4089        - `OverviewUserInfo()` method
4090
4091        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4092                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4093                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4094        """
4095        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4096
4097        self.body = str({})
4098        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4099        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4100
4101        if self.moreDebug:
4102            uLogger.debug("Records with limits of current tariff successfully received")
4103
4104        return rawTariffLimits
4105
4106    def RequestBondCoupons(self, iJSON: dict) -> dict:
4107        """
4108        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4109        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4110        All dates are in UTC timezone.
4111
4112        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4113        Documentation:
4114        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4115        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4116
4117        See also: `ExtendBondsData()`.
4118
4119        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4120                      If raw iJSON is not data of bond then server returns an error [400] with message:
4121                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4122        :return: dictionary with bond payment calendar. Response example
4123                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4124                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4125                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4126                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4127        """
4128        if iJSON["figi"] is None or not iJSON["figi"]:
4129            uLogger.error("FIGI must be defined for using this method!")
4130            raise Exception("FIGI required")
4131
4132        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4133        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4134
4135        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4136            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4137            self._figi,
4138            startDate,
4139            endDate,
4140        ))
4141
4142        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4143        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4144        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4145
4146        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4147            uLogger.warning("Instrument type is not bond!")
4148
4149        else:
4150            if self.moreDebug:
4151                uLogger.debug("Records about bond payment calendar successfully received")
4152
4153        return calendar
4154
4155    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4156        """
4157        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4158        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4159        coupon yields, current yields and some statistics etc.
4160
4161        WARNING! This is too long operation if a lot of bonds requested from broker server.
4162
4163        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4164
4165        :param instruments: list of strings with tickers or FIGIs.
4166        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4167                     for further used by data scientists or stock analytics.
4168        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4169                 In XLSX-file and Pandas DataFrame fields mean:
4170                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4171                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4172        """
4173        if instruments is None or not instruments:
4174            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4175            raise Exception("Ticker or FIGI required")
4176
4177        if isinstance(instruments, str):
4178            instruments = [instruments]
4179
4180        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4181
4182        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4183
4184        iCount = len(uniqueInstruments)
4185        tooLong = iCount >= 20
4186        if tooLong:
4187            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4188
4189        bonds = None
4190        for i, self._figi in enumerate(uniqueInstruments):
4191            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4192
4193            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4194                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4195                rawBond = self.SearchByFIGI(requestPrice=True)
4196
4197                # Widen raw data with UTC current time (iData["actualDateTime"]):
4198                actualDate = datetime.now(tzutc())
4199                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4200
4201                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4202                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4203
4204                # Replace some values with human-readable:
4205                iData["nominalCurrency"] = iData["nominal"]["currency"]
4206                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4207                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4208                iData["aciCurrency"] = iData["aciValue"]["currency"]
4209                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4210                iData["issueSize"] = int(iData["issueSize"])
4211                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4212                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4213                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4214                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4215                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4216                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4217                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4218                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4219                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4220                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4221
4222                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4223                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4224                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4225                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4226                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4227                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4228                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4229                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4230                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4231                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4232                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4233
4234                # Widen raw data with calendar data from `rawCalendar` values:
4235                calendarData = []
4236                if "events" in iData["rawCalendar"].keys():
4237                    for item in iData["rawCalendar"]["events"]:
4238                        calendarData.append({
4239                            "couponDate": item["couponDate"],
4240                            "couponNumber": int(item["couponNumber"]),
4241                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4242                            "payCurrency": item["payOneBond"]["currency"],
4243                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4244                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4245                            "couponStartDate": item["couponStartDate"],
4246                            "couponEndDate": item["couponEndDate"],
4247                            "couponPeriod": item["couponPeriod"],
4248                        })
4249
4250                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4251                    if "maturityDate" not in iData.keys():
4252                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4253
4254                # Widen raw data with Coupon Rate.
4255                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4256                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4257                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4258                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4259
4260                # Widen raw data with Yield to Maturity (YTM) on current date.
4261                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4262                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4263                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4264                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4265                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4266                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4267
4268                iData["calendar"] = calendarData  # adds calendar at the end
4269
4270                # Remove not used data:
4271                iData.pop("uid")
4272                iData.pop("positionUid")
4273                iData.pop("currentPrice")
4274                iData.pop("rawCalendar")
4275
4276                colNames = list(iData.keys())
4277                if bonds is None:
4278                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4279
4280                else:
4281                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4282
4283            else:
4284                uLogger.warning("Instrument is not a bond!")
4285
4286            processed = round(100 * (i + 1) / iCount, 1)
4287            if tooLong and processed % 5 == 0:
4288                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4289
4290            else:
4291                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4292
4293        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4294
4295        # Saving bonds from Pandas DataFrame to XLSX sheet:
4296        if xlsx and self.bondsXLSXFile:
4297            with pd.ExcelWriter(
4298                    path=self.bondsXLSXFile,
4299                    date_format=TKS_DATE_FORMAT,
4300                    datetime_format=TKS_DATE_TIME_FORMAT,
4301                    mode="w",
4302            ) as writer:
4303                bonds.to_excel(
4304                    writer,
4305                    sheet_name="Extended bonds data",
4306                    index=True,
4307                    encoding="UTF-8",
4308                    freeze_panes=(1, 1),
4309                )  # saving as XLSX-file with freeze first row and column as headers
4310
4311            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4312
4313        return bonds
4314
4315    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4316        """
4317        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4318
4319        WARNING! This is too long operation if a lot of bonds requested from broker server.
4320
4321        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4322
4323        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4324                        extended information about bonds: main info, current prices, bond payment calendar,
4325                        coupon yields, current yields and some statistics etc.
4326                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4327        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4328                     for further used by data scientists or stock analytics.
4329        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4330        """
4331        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4332            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4333
4334        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4335
4336        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4337        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4338        calendar = None
4339        for bond in extBonds.iterrows():
4340            for item in bond[1]["calendar"]:
4341                cData = {
4342                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4343                    "couponDate": item["couponDate"],
4344                    "figi": bond[1]["figi"],
4345                    "ticker": bond[1]["ticker"],
4346                    "name": bond[1]["name"],
4347                    "couponNumber": item["couponNumber"],
4348                    "payOneBond": item["payOneBond"],
4349                    "payCurrency": item["payCurrency"],
4350                    "couponType": item["couponType"],
4351                    "couponPeriod": item["couponPeriod"],
4352                    "fixDate": item["fixDate"],
4353                    "couponStartDate": item["couponStartDate"],
4354                    "couponEndDate": item["couponEndDate"],
4355                }
4356
4357                if calendar is None:
4358                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4359
4360                else:
4361                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4362
4363        if calendar is not None:
4364            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4365
4366            # Saving calendar from Pandas DataFrame to XLSX sheet:
4367            if xlsx:
4368                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4369
4370                with pd.ExcelWriter(
4371                        path=xlsxCalendarFile,
4372                        date_format=TKS_DATE_FORMAT,
4373                        datetime_format=TKS_DATE_TIME_FORMAT,
4374                        mode="w",
4375                ) as writer:
4376                    humanReadable = calendar.copy(deep=True)
4377                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4378                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4379                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4380                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4381                    humanReadable.columns = colNames  # human-readable column names
4382
4383                    humanReadable.to_excel(
4384                        writer,
4385                        sheet_name="Bond payments calendar",
4386                        index=False,
4387                        encoding="UTF-8",
4388                        freeze_panes=(1, 2),
4389                    )  # saving as XLSX-file with freeze first row and column as headers
4390
4391                    del humanReadable  # release df in memory
4392
4393                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4394
4395        return calendar
4396
4397    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4398        """
4399        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4400        Also, creates Markdown file with calendar data, `calendar.md` by default.
4401
4402        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4403
4404        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4405                        extended information about bonds: main info, current prices, bond payment calendar,
4406                        coupon yields, current yields and some statistics etc.
4407                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4408        :param show: if `True` then also printing bonds payment calendar to the console,
4409                     otherwise save to file `calendarFile` only. `False` by default.
4410        :return: multilines text in Markdown format with bonds payment calendar as a table.
4411        """
4412        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4413            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4414
4415        infoText = "# Bond payments calendar\n\n"
4416
4417        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4418
4419        if not (calendar is None or calendar.empty):
4420            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4421
4422            info = [
4423                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4424                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4425                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4426            ]
4427
4428            newMonth = False
4429            notOneBond = calendar["figi"].nunique() > 1
4430            for i, bond in enumerate(calendar.iterrows()):
4431                if newMonth and notOneBond:
4432                    info.append(splitLine)
4433
4434                info.append(
4435                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4436                        "  √" if bond[1]["paid"] else "  —",
4437                        bond[1]["couponDate"].split("T")[0],
4438                        bond[1]["figi"],
4439                        bond[1]["ticker"],
4440                        bond[1]["couponNumber"],
4441                        "{} {}".format(
4442                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4443                            bond[1]["payCurrency"],
4444                        ),
4445                        bond[1]["couponType"],
4446                        bond[1]["couponPeriod"],
4447                        bond[1]["fixDate"].split("T")[0],
4448                    )
4449                )
4450
4451                if i < len(calendar.values) - 1:
4452                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4453                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4454                    newMonth = False if curDate.month == nextDate.month else True
4455
4456                else:
4457                    newMonth = False
4458
4459            infoText += "".join(info)
4460
4461            if show:
4462                uLogger.info("{}".format(infoText))
4463
4464            if self.calendarFile is not None:
4465                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4466                    fH.write(infoText)
4467
4468                uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4469
4470                if self.useHTMLReports:
4471                    htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html"
4472                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4473                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText))
4474
4475                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4476
4477        else:
4478            infoText += "No data\n"
4479
4480        return infoText
4481
4482    def OverviewAccounts(self, show: bool = False) -> dict:
4483        """
4484        Method for parsing and show simple table with all available user accounts.
4485
4486        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4487
4488        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4489        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4490                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4491                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4492                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4493                                                        "closed": "—", "access": "Full access" }, ...}}`
4494        """
4495        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4496
4497        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4498        accounts = {
4499            item["id"]: {
4500                "type": TKS_ACCOUNT_TYPES[item["type"]],
4501                "name": item["name"],
4502                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4503                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4504                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4505                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4506            } for item in rawAccounts["accounts"]
4507        }
4508
4509        # Raw and parsed data with some fields replaced in "stat" section:
4510        view = {
4511            "rawAccounts": rawAccounts,
4512            "stat": accounts,
4513        }
4514
4515        # --- Prepare simple text table with only accounts data in human-readable format:
4516        if show:
4517            info = [
4518                "# User accounts\n\n",
4519                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4520                "| Account ID   | Type                      | Status                    | Name                           |\n",
4521                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4522            ]
4523
4524            for account in view["stat"].keys():
4525                info.extend([
4526                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4527                        account,
4528                        view["stat"][account]["type"],
4529                        view["stat"][account]["status"],
4530                        view["stat"][account]["name"],
4531                    )
4532                ])
4533
4534            infoText = "".join(info)
4535
4536            uLogger.info(infoText)
4537
4538            if self.userAccountsFile:
4539                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4540                    fH.write(infoText)
4541
4542                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4543
4544                if self.useHTMLReports:
4545                    htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html"
4546                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4547                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText))
4548
4549                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4550
4551        return view
4552
4553    def OverviewUserInfo(self, show: bool = False) -> dict:
4554        """
4555        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4556
4557        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4558
4559        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4560        :return: dict with raw parsed data from server and some calculated statistics about it.
4561        """
4562        overview = self.Overview(show=False)  # Request current user portfolio for the ability to calculate missing funds
4563        tmpTicker = self._ticker
4564        self._ticker = "RUB000UTSTOM"  # This instrument show in rub how much money cost current margin
4565        missing = self.GetInstrumentFromPortfolio(portfolio=overview)
4566        self._ticker = tmpTicker
4567
4568        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4569        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4570        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4571        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4572        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4573        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4574
4575        # This is dict with parsed common user data:
4576        userInfo = {
4577            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4578            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4579            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4580            "tariff": rawUserInfo["tariff"],
4581        }
4582
4583        # This is an array of dict with parsed margin statuses for every account IDs:
4584        margins = {}
4585        for accountId in accounts.keys():
4586            if rawMargins[accountId]:
4587                margins[accountId] = {
4588                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4589                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4590                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4591                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4592                    "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4593                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4594                    "missing": missing["volume"],
4595                }
4596
4597            else:
4598                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4599
4600        unary = {}  # unary-connection limits
4601        for item in rawTariffLimits["unaryLimits"]:
4602            if item["limitPerMinute"] in unary.keys():
4603                unary[item["limitPerMinute"]].extend(item["methods"])
4604
4605            else:
4606                unary[item["limitPerMinute"]] = item["methods"]
4607
4608        stream = {}  # stream-connection limits
4609        for item in rawTariffLimits["streamLimits"]:
4610            if item["limit"] in stream.keys():
4611                stream[item["limit"]].extend(item["streams"])
4612
4613            else:
4614                stream[item["limit"]] = item["streams"]
4615
4616        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4617        limits = {
4618            "unary": unary,
4619            "stream": stream,
4620        }
4621
4622        # Raw and parsed data as an output result:
4623        view = {
4624            "rawUserInfo": rawUserInfo,
4625            "rawAccounts": rawAccounts,
4626            "rawMargins": rawMargins,
4627            "rawTariffLimits": rawTariffLimits,
4628            "stat": {
4629                "overview": overview,
4630                "userInfo": userInfo,
4631                "accounts": accounts,
4632                "margins": margins,
4633                "limits": limits,
4634            },
4635        }
4636
4637        # --- Prepare text table with user information in human-readable format:
4638        if show:
4639            info = [
4640                "# Full user information\n\n",
4641                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4642                "## Common information\n\n",
4643                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4644                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4645                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4646                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4647                "\n## User accounts\n\n",
4648            ]
4649
4650            for account in view["stat"]["accounts"].keys():
4651                info.extend([
4652                    "### ID: [{}]\n\n".format(account),
4653                    "| Parameters           | Values                                                       |\n",
4654                    "|----------------------|--------------------------------------------------------------|\n",
4655                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4656                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4657                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4658                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4659                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4660                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4661                ])
4662
4663                if margins[account]:
4664                    info.extend([
4665                        "| Margin status:       | Enabled                                                      |\n",
4666                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4667                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4668                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4669                        "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])),
4670                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4671                        "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])),
4672                    ])
4673
4674                else:
4675                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4676
4677            info.extend([
4678                "\n## Current user tariff limits\n",
4679                "\n### See also\n",
4680                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4681                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4682                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4683                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4684                "\n### Unary limits\n",
4685            ])
4686
4687            if unary:
4688                for key, values in sorted(unary.items()):
4689                    info.append("\n* Max requests per minute: {}\n".format(key))
4690
4691                    for value in values:
4692                        info.append("  - {}\n".format(value))
4693
4694            else:
4695                info.append("\nNot available\n")
4696
4697            info.append("\n### Stream limits\n")
4698
4699            if stream:
4700                for key, values in sorted(stream.items()):
4701                    info.append("\n* Max stream connections: {}\n".format(key))
4702
4703                    for value in values:
4704                        info.append("  - {}\n".format(value))
4705
4706            else:
4707                info.append("\nNot available\n")
4708
4709            infoText = "".join(info)
4710
4711            uLogger.info(infoText)
4712
4713            if self.userInfoFile:
4714                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4715                    fH.write(infoText)
4716
4717                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4718
4719                if self.useHTMLReports:
4720                    htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html"
4721                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4722                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText))
4723
4724                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4725
4726        return view
4727
4728
4729class Args:
4730    """
4731    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4732    """
4733    def __init__(self, **kwargs):
4734        self.__dict__.update(kwargs)
4735
4736    def __getattr__(self, item):
4737        return None
4738
4739
4740def ParseArgs():
4741    """This function get and parse command line keys."""
4742    parser = ArgumentParser()  # command-line string parser
4743
4744    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4745    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4746
4747    # --- options:
4748
4749    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4750    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4751    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4752
4753    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4754    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4755
4756    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4757    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4758
4759    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4760    parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.")
4761
4762    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4763    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4764    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4765
4766    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4767    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4768
4769    # --- commands:
4770
4771    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4772
4773    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4774    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4775    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4776    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4777    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4778    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4779    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4780    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4781
4782    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4783    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4784    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4785    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4786    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4787    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4788
4789    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4790    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4791    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4792    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4793
4794    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4795    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4796    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4797
4798    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4799    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4800    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4801    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4802    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4803    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4804    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4805
4806    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4807    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4808    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4809    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4810    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.")
4811
4812    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4813    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4814    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4815
4816    cmdArgs = parser.parse_args()
4817    return cmdArgs
4818
4819
4820def Main(**kwargs):
4821    """
4822    Main function for work with TKSBrokerAPI in the console.
4823
4824    See examples:
4825    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4826    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4827    """
4828    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4829
4830    if args.debug_level:
4831        uLogger.level = 10  # always debug level by default
4832        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4833
4834    exitCode = 0
4835    start = datetime.now(tzutc())
4836    uLogger.debug("=-" * 50)
4837    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4838        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4839        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4840    ))
4841
4842    # trying to calculate full current version:
4843    buildVersion = __version__
4844    try:
4845        v = version("tksbrokerapi")
4846        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4847
4848    except Exception:
4849        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4850
4851    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4852    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4853
4854    try:
4855        if args.version:
4856            print("TKSBrokerAPI {}".format(buildVersion))
4857            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4858
4859        else:
4860            # Init class for trading with Tinkoff Broker:
4861            trader = TinkoffBrokerServer(
4862                token=args.token,
4863                accountId=args.account_id,
4864                useCache=not args.no_cache,
4865            )
4866
4867            # --- set some options:
4868
4869            if args.more:
4870                trader.moreDebug = True
4871                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4872
4873            if args.html:
4874                trader.useHTMLReports = True
4875
4876            if args.ticker:
4877                ticker = str(args.ticker).upper()  # Tickers may be upper case only
4878
4879                if ticker in trader.aliasesKeys:
4880                    trader.ticker = trader.aliases[ticker]  # Replace some tickers with its aliases
4881
4882                else:
4883                    trader.ticker = ticker
4884
4885            if args.figi:
4886                trader.figi = str(args.figi).upper()  # FIGIs may be upper case only
4887
4888            if args.depth is not None:
4889                trader.depth = args.depth
4890
4891            # --- do one command:
4892
4893            if args.list:
4894                if args.output is not None:
4895                    trader.instrumentsFile = args.output
4896
4897                trader.ShowInstrumentsInfo(show=True)
4898
4899            elif args.list_xlsx:
4900                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4901
4902            elif args.bonds_xlsx is not None:
4903                if args.output is not None:
4904                    trader.bondsXLSXFile = args.output
4905
4906                if len(args.bonds_xlsx) == 0:
4907                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4908
4909                else:
4910                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4911
4912            elif args.search:
4913                if args.output is not None:
4914                    trader.searchResultsFile = args.output
4915
4916                trader.SearchInstruments(pattern=args.search[0], show=True)
4917
4918            elif args.info:
4919                if not (args.ticker or args.figi):
4920                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4921                    raise Exception("Ticker or FIGI required")
4922
4923                if args.output is not None:
4924                    trader.infoFile = args.output
4925
4926                if args.ticker:
4927                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4928
4929                else:
4930                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4931
4932            elif args.calendar is not None:
4933                if args.output is not None:
4934                    trader.calendarFile = args.output
4935
4936                if len(args.calendar) == 0:
4937                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4938
4939                else:
4940                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4941
4942                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4943
4944            elif args.price:
4945                if not (args.ticker or args.figi):
4946                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4947                    raise Exception("Ticker or FIGI required")
4948
4949                trader.GetCurrentPrices(show=True)
4950
4951            elif args.prices is not None:
4952                if args.output is not None:
4953                    trader.pricesFile = args.output
4954
4955                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4956
4957            elif args.overview:
4958                if args.output is not None:
4959                    trader.overviewFile = args.output
4960
4961                trader.Overview(show=True, details="full")
4962
4963            elif args.overview_digest:
4964                if args.output is not None:
4965                    trader.overviewDigestFile = args.output
4966
4967                trader.Overview(show=True, details="digest")
4968
4969            elif args.overview_positions:
4970                if args.output is not None:
4971                    trader.overviewPositionsFile = args.output
4972
4973                trader.Overview(show=True, details="positions")
4974
4975            elif args.overview_orders:
4976                if args.output is not None:
4977                    trader.overviewOrdersFile = args.output
4978
4979                trader.Overview(show=True, details="orders")
4980
4981            elif args.overview_analytics:
4982                if args.output is not None:
4983                    trader.overviewAnalyticsFile = args.output
4984
4985                trader.Overview(show=True, details="analytics")
4986
4987            elif args.overview_calendar:
4988                if args.output is not None:
4989                    trader.overviewAnalyticsFile = args.output
4990
4991                trader.Overview(show=True, details="calendar")
4992
4993            elif args.deals is not None:
4994                if args.output is not None:
4995                    trader.reportFile = args.output
4996
4997                if 0 <= len(args.deals) < 3:
4998                    trader.Deals(
4999                        start=args.deals[0] if len(args.deals) >= 1 else None,
5000                        end=args.deals[1] if len(args.deals) == 2 else None,
5001                        show=True,  # Always show deals report in console
5002                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
5003                    )
5004
5005                else:
5006                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5007                    raise Exception("Incorrect value")
5008
5009            elif args.history is not None:
5010                if args.output is not None:
5011                    trader.historyFile = args.output
5012
5013                if 0 <= len(args.history) < 3:
5014                    dataReceived = trader.History(
5015                        start=args.history[0] if len(args.history) >= 1 else None,
5016                        end=args.history[1] if len(args.history) == 2 else None,
5017                        interval="hour" if args.interval is None or not args.interval else args.interval,
5018                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
5019                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
5020                        show=True,  # shows all downloaded candles in console
5021                    )
5022
5023                    if args.render_chart is not None and dataReceived is not None:
5024                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5025
5026                        trader.ShowHistoryChart(
5027                            candles=dataReceived,
5028                            interact=iChart,
5029                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5030                        )
5031
5032                else:
5033                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5034                    raise Exception("Incorrect value")
5035
5036            elif args.load_history is not None:
5037                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
5038
5039                if args.render_chart is not None and histData is not None:
5040                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5041                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
5042
5043                    trader.ShowHistoryChart(
5044                        candles=histData,
5045                        interact=iChart,
5046                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5047                    )
5048
5049            elif args.trade is not None:
5050                if 1 <= len(args.trade) <= 5:
5051                    trader.Trade(
5052                        operation=args.trade[0],
5053                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
5054                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
5055                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
5056                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
5057                    )
5058
5059                else:
5060                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5061
5062            elif args.buy is not None:
5063                if 0 <= len(args.buy) <= 4:
5064                    trader.Buy(
5065                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
5066                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
5067                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
5068                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
5069                    )
5070
5071                else:
5072                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5073
5074            elif args.sell is not None:
5075                if 0 <= len(args.sell) <= 4:
5076                    trader.Sell(
5077                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
5078                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
5079                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
5080                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
5081                    )
5082
5083                else:
5084                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5085
5086            elif args.order:
5087                if 4 <= len(args.order) <= 7:
5088                    trader.Order(
5089                        operation=args.order[0],
5090                        orderType=args.order[1],
5091                        lots=int(args.order[2]),
5092                        targetPrice=float(args.order[3]),
5093                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
5094                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
5095                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
5096                    )
5097
5098                else:
5099                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
5100
5101            elif args.buy_limit:
5102                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
5103
5104            elif args.sell_limit:
5105                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
5106
5107            elif args.buy_stop:
5108                if 2 <= len(args.buy_stop) <= 7:
5109                    trader.BuyStop(
5110                        lots=int(args.buy_stop[0]),
5111                        targetPrice=float(args.buy_stop[1]),
5112                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
5113                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
5114                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
5115                    )
5116
5117                else:
5118                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5119
5120            elif args.sell_stop:
5121                if 2 <= len(args.sell_stop) <= 7:
5122                    trader.SellStop(
5123                        lots=int(args.sell_stop[0]),
5124                        targetPrice=float(args.sell_stop[1]),
5125                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
5126                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
5127                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
5128                    )
5129
5130                else:
5131                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
5132
5133            # elif args.buy_order_grid is not None:
5134            #     # update order grid work with api v2
5135            #     if len(args.buy_order_grid) == 2:
5136            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
5137            #
5138            #         for order in orderParams:
5139            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
5140            #
5141            #     else:
5142            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5143            #
5144            # elif args.sell_order_grid is not None:
5145            #     # update order grid work with api v2
5146            #     if len(args.sell_order_grid) >= 2:
5147            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
5148            #
5149            #         for order in orderParams:
5150            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
5151            #
5152            #     else:
5153            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5154
5155            elif args.close_order is not None:
5156                trader.CloseOrders(args.close_order)  # close only one order
5157
5158            elif args.close_orders is not None:
5159                trader.CloseOrders(args.close_orders)  # close list of orders
5160
5161            elif args.close_trade:
5162                if not (args.ticker or args.figi):
5163                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5164                    raise Exception("Ticker or FIGI required")
5165
5166                if args.ticker:
5167                    trader.CloseTrades([str(args.ticker).upper()])  # close only one trade by ticker (priority)
5168
5169                else:
5170                    trader.CloseTrades([str(args.figi).upper()])  # close only one trade by FIGI
5171
5172            elif args.close_trades is not None:
5173                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
5174
5175            elif args.close_all is not None:
5176                if args.ticker:
5177                    trader.CloseAllByTicker(instrument=str(args.ticker).upper())
5178
5179                elif args.figi:
5180                    trader.CloseAllByFIGI(instrument=str(args.figi).upper())
5181
5182                else:
5183                    trader.CloseAll(*args.close_all)
5184
5185            elif args.limits:
5186                if args.output is not None:
5187                    trader.withdrawalLimitsFile = args.output
5188
5189                trader.OverviewLimits(show=True)
5190
5191            elif args.user_info:
5192                if args.output is not None:
5193                    trader.userInfoFile = args.output
5194
5195                trader.OverviewUserInfo(show=True)
5196
5197            elif args.account:
5198                if args.output is not None:
5199                    trader.userAccountsFile = args.output
5200
5201                trader.OverviewAccounts(show=True)
5202
5203            else:
5204                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
5205                raise Exception("There is no command to execute")
5206
5207    except Exception:
5208        trace = tb.format_exc()
5209        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
5210            if e in trace:
5211                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
5212                break
5213
5214        uLogger.debug(trace)
5215        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
5216        exitCode = 255  # an error occurred, must be open a ticket for this issue
5217
5218    finally:
5219        finish = datetime.now(tzutc())
5220
5221        if exitCode == 0:
5222            if args.more:
5223                uLogger.debug("All operations were finished success (summary code is 0).")
5224
5225        else:
5226            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
5227                os.path.abspath(uLog.defaultLogFile), exitCode,
5228            ))
5229
5230        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
5231        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
5232            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
5233            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
5234        ))
5235        uLogger.debug("=-" * 50)
5236
5237        if not kwargs:
5238            sys.exit(exitCode)
5239
5240        else:
5241            return exitCode
5242
5243
5244if __name__ == "__main__":
5245    Main()
class TinkoffBrokerServer:
  78class TinkoffBrokerServer:
  79    """
  80    This class implements methods to work with Tinkoff broker server.
  81
  82    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
  83
  84    About `token`: https://tinkoff.github.io/investAPI/token/
  85    """
  86    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
  87        """
  88        Main class init.
  89
  90        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
  91        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
  92                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
  93        :param useCache: use default cache file with raw data to use instead of `iList`.
  94                         True by default. Cache is auto-update if new day has come.
  95                         If you don't want to use cache and always updates raw data then set `useCache=False`.
  96        :param defaultCache: path to default cache file. `dump.json` by default.
  97        """
  98        if token is None or not token:
  99            try:
 100                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 101                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 102
 103            except KeyError:
 104                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 105                raise Exception("Token required")
 106
 107        else:
 108            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 109            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 110
 111        if accountId is None or not accountId:
 112            try:
 113                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 114                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 115
 116            except KeyError:
 117                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 118
 119        else:
 120            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 121            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 122
 123        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 124        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 125
 126        Latest version: https://pypi.org/project/tksbrokerapi/
 127        """
 128
 129        self.__lock = Lock()  # initialize multiprocessing mutex lock
 130
 131        self.aliases = TKS_TICKER_ALIASES
 132        """Some aliases instead official tickers.
 133
 134        See also: `TKSEnums.TKS_TICKER_ALIASES`
 135        """
 136
 137        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 138
 139        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 140
 141        self._ticker = ""
 142        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 143
 144        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 145        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 146
 147        See also: `SearchByTicker()`, `SearchInstruments()`.
 148        """
 149
 150        self._figi = ""
 151        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 152
 153        See also: `SearchByFIGI()`, `SearchInstruments()`.
 154        """
 155
 156        self.depth = 1
 157        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 158
 159        See also: `GetCurrentPrices()`.
 160        """
 161
 162        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 163        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 164
 165        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 166        """
 167
 168        uLogger.debug("Broker API server: {}".format(self.server))
 169
 170        self.timeout = 15
 171        """Server operations timeout in seconds. Default: `15`.
 172
 173        See also: `SendAPIRequest()`.
 174        """
 175
 176        self.headers = {
 177            "Content-Type": "application/json",
 178            "accept": "application/json",
 179            "Authorization": "Bearer {}".format(self.token),
 180            "x-app-name": "Tim55667757.TKSBrokerAPI",
 181        }
 182        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 183
 184        See also: `SendAPIRequest()`.
 185        """
 186
 187        self.body = None
 188        """Request body which send to broker server. Default: `None`.
 189
 190        See also: `SendAPIRequest()`.
 191        """
 192
 193        self.moreDebug = False
 194        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 195
 196        self.useHTMLReports = False
 197        """
 198        If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default.
 199        
 200        See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
 201        """
 202
 203        self.historyFile = None
 204        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 205
 206        See also: `History()`.
 207        """
 208
 209        self.htmlHistoryFile = "index.html"
 210        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 211
 212        See also: `ShowHistoryChart()`.
 213        """
 214
 215        self.instrumentsFile = "instruments.md"
 216        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 217
 218        See also: `ShowInstrumentsInfo()`.
 219        """
 220
 221        self.searchResultsFile = "search-results.md"
 222        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 223
 224        See also: `SearchInstruments()`.
 225        """
 226
 227        self.pricesFile = "prices.md"
 228        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 229
 230        See also: `GetListOfPrices()`.
 231        """
 232
 233        self.infoFile = "info.md"
 234        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 235
 236        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 237        """
 238
 239        self.bondsXLSXFile = "ext-bonds.xlsx"
 240        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 241        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 242
 243        See also: `ExtendBondsData()`.
 244        """
 245
 246        self.calendarFile = "calendar.md"
 247        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 248        
 249        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 250
 251        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 252        """
 253
 254        self.overviewFile = "overview.md"
 255        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 256
 257        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 258        """
 259
 260        self.overviewDigestFile = "overview-digest.md"
 261        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 262
 263        See also: `Overview()` with parameter `details="digest"`.
 264        """
 265
 266        self.overviewPositionsFile = "overview-positions.md"
 267        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 268
 269        See also: `Overview()` with parameter `details="positions"`.
 270        """
 271
 272        self.overviewOrdersFile = "overview-orders.md"
 273        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 274
 275        See also: `Overview()` with parameter `details="orders"`.
 276        """
 277
 278        self.overviewAnalyticsFile = "overview-analytics.md"
 279        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 280
 281        See also: `Overview()` with parameter `details="analytics"`.
 282        """
 283
 284        self.overviewBondsCalendarFile = "overview-calendar.md"
 285        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 286
 287        See also: `Overview()` with parameter `details="calendar"`.
 288        """
 289
 290        self.reportFile = "deals.md"
 291        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 292
 293        See also: `Deals()`.
 294        """
 295
 296        self.withdrawalLimitsFile = "limits.md"
 297        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 298
 299        See also: `OverviewLimits()` and `RequestLimits()`.
 300        """
 301
 302        self.userInfoFile = "user-info.md"
 303        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 304
 305        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 306        """
 307
 308        self.userAccountsFile = "accounts.md"
 309        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 310
 311        See also: `OverviewAccounts()`, `RequestAccounts()`.
 312        """
 313
 314        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 315        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 316
 317        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 318
 319        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 320        """
 321
 322        self.iList = None  # init iList for raw instruments data
 323        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 324        
 325        See also: `Listing()`, `DumpInstruments()`.
 326        """
 327
 328        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 329        if useCache:
 330            if os.path.exists(self.iListDumpFile):
 331                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 332                curTime = datetime.now(tzutc())
 333
 334                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 335                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 336
 337                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 338
 339                else:
 340                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 341
 342                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 343                        os.path.abspath(self.iListDumpFile),
 344                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 345                    ))
 346
 347            else:
 348                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 349                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 350
 351        else:
 352            self.iList = self.Listing()  # request new raw instruments data from broker server
 353            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 354
 355        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 356        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 357
 358        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 359        """
 360
 361    @property
 362    def ticker(self) -> str:
 363        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 364
 365        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 366        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 367
 368        See also: `SearchByTicker()`, `SearchInstruments()`.
 369        """
 370        return self._ticker
 371
 372    @ticker.setter
 373    def ticker(self, value):
 374        """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 375
 376        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 377        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 378
 379        See also: `SearchByTicker()`, `SearchInstruments()`.
 380        """
 381        self._ticker = str(value).upper()  # Tickers may be upper case only
 382
 383    @property
 384    def figi(self) -> str:
 385        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 386
 387        See also: `SearchByFIGI()`, `SearchInstruments()`.
 388        """
 389        return self._figi
 390
 391    @figi.setter
 392    def figi(self, value):
 393        """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 394
 395        See also: `SearchByFIGI()`, `SearchInstruments()`.
 396        """
 397        self._figi = str(value).upper()  # FIGI may be upper case only
 398
 399    def _ParseJSON(self, rawData="{}") -> dict:
 400        """
 401        Parse JSON from response string.
 402
 403        :param rawData: this is a string with JSON-formatted text.
 404        :return: JSON (dictionary), parsed from server response string.
 405        """
 406        responseJSON = json.loads(rawData) if rawData else {}
 407
 408        if self.moreDebug:
 409            uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 410
 411        return responseJSON
 412
 413    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 414        """
 415        Send GET or POST request to broker server and receive JSON object.
 416
 417        self.header: must be defining with dictionary of headers.
 418        self.body: if define then used as request body. None by default.
 419        self.timeout: global request timeout, 15 seconds by default.
 420        :param url: url with REST request.
 421        :param reqType: send "GET" or "POST" request. "GET" by default.
 422        :param retry: how many times retry after first request if an 5xx server errors occurred.
 423        :param pause: sleep time in seconds between retries.
 424        :return: response JSON (dictionary) from broker.
 425        """
 426        if reqType.upper() not in ("GET", "POST"):
 427            uLogger.error("You can define request type: `GET` or `POST`!")
 428            raise Exception("Incorrect value")
 429
 430        if self.moreDebug:
 431            uLogger.debug("Request parameters:")
 432            uLogger.debug("    - REST API URL: {}".format(url))
 433            uLogger.debug("    - request type: {}".format(reqType))
 434            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 435            uLogger.debug("    - body:\n{}".format(self.body))
 436
 437        # fast hack to avoid all operations with some tickers/FIGI
 438        responseJSON = {}
 439        oK = True
 440        for item in self.exclude:
 441            if item in url:
 442                if self.moreDebug:
 443                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 444
 445                oK = False
 446                break
 447
 448        if oK:
 449            with self.__lock:  # acquire the mutex lock
 450                counter = 0
 451                response = None
 452                errMsg = ""
 453
 454                while not response and counter <= retry:
 455                    if reqType == "GET":
 456                        response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 457
 458                    if reqType == "POST":
 459                        response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 460
 461                    if self.moreDebug:
 462                        uLogger.debug("Response:")
 463                        uLogger.debug("    - status code: {}".format(response.status_code))
 464                        uLogger.debug("    - reason: {}".format(response.reason))
 465                        uLogger.debug("    - body length: {}".format(len(response.text)))
 466                        uLogger.debug("    - headers:\n{}".format(response.headers))
 467
 468                    # Server returns some headers:
 469                    # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 470                    # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 471                    # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 472                    # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 473                    if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 474                        rateLimitWait = int(response.headers["x-ratelimit-reset"])
 475                        uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 476                        sleep(rateLimitWait)
 477
 478                    # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 479                    if 400 <= response.status_code < 500:
 480                        msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 481                        uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 482
 483                        if "code" in response.text and "message" in response.text:
 484                            msgDict = self._ParseJSON(rawData=response.text)
 485                            uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
 486
 487                        counter = retry + 1  # do not retry for 4xx errors
 488
 489                    if 500 <= response.status_code < 600:
 490                        errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 491                        uLogger.debug("    - not oK, {}".format(errMsg))
 492
 493                        if "code" in response.text and "message" in response.text:
 494                            errMsgDict = self._ParseJSON(rawData=response.text)
 495                            uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
 496
 497                        counter += 1
 498
 499                        if counter <= retry:
 500                            uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 501                            sleep(pause)
 502
 503                responseJSON = self._ParseJSON(rawData=response.text)
 504
 505                if errMsg:
 506                    uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 507                    uLogger.error("    - not oK, {}".format(errMsg))
 508
 509        return responseJSON
 510
 511    def _IUpdater(self, iType: str) -> tuple:
 512        """
 513        Request instrument by type from server. See available API methods for instruments:
 514        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 515        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 516        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 517        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 518        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 519
 520        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 521        :return: tuple with iType name and list of available instruments of current type for defined user token.
 522        """
 523        result = []
 524
 525        if iType in TKS_INSTRUMENTS:
 526            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 527
 528            # all instruments have the same body in API v2 requests:
 529            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 530            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 531            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 532
 533        return iType, result
 534
 535    def _IWrapper(self, kwargs):
 536        """
 537        Wrapper runs instrument's update method `_IUpdater()`.
 538        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 539        """
 540        return self._IUpdater(**kwargs)
 541
 542    def Listing(self) -> dict:
 543        """
 544        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 545
 546        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 547        """
 548        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 549        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 550
 551        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 552        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 553        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 554
 555        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 556        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 557        poolUpdater.close()  # close the thread pool
 558        poolUpdater.join()  # wait a moment until all data returns from threads
 559
 560        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 561        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 562        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 563
 564        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 565        for iType in iList.keys():
 566            for ticker in iList[iType]:
 567                iList[iType][ticker]["type"] = iType
 568
 569                if "minPriceIncrement" in iList[iType][ticker].keys():
 570                    iList[iType][ticker]["step"] = NanoToFloat(
 571                        iList[iType][ticker]["minPriceIncrement"]["units"],
 572                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 573                    )
 574
 575                else:
 576                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 577
 578        return iList
 579
 580    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 581        """
 582        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 583
 584        See also: `DumpInstruments()`, `Listing()`.
 585
 586        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 587                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 588        """
 589        if self.iListDumpFile is None or not self.iListDumpFile:
 590            uLogger.error("Output name of dump file must be defined!")
 591            raise Exception("Filename required")
 592
 593        if not self.iList or forceUpdate:
 594            self.iList = self.Listing()
 595
 596        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 597
 598        # Save as XLSX with separated sheets for every type of instruments:
 599        with pd.ExcelWriter(
 600                path=xlsxDumpFile,
 601                date_format=TKS_DATE_FORMAT,
 602                datetime_format=TKS_DATE_TIME_FORMAT,
 603                mode="w",
 604        ) as writer:
 605            for iType in TKS_INSTRUMENTS:
 606                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 607                df = df[sorted(df)]  # sorted by column names
 608                df = df.applymap(
 609                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 610                    na_action="ignore",
 611                )  # converting numbers from nano-type to float in every cell
 612                df.to_excel(
 613                    writer,
 614                    sheet_name=iType,
 615                    encoding="UTF-8",
 616                    freeze_panes=(1, 1),
 617                )  # saving as XLSX-file with freeze first row and column as headers
 618
 619        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 620
 621    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 622        """
 623        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 624        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 625
 626        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 627
 628        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 629                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 630        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 631        """
 632        if self.iListDumpFile is None or not self.iListDumpFile:
 633            uLogger.error("Output name of dump file must be defined!")
 634            raise Exception("Filename required")
 635
 636        if not self.iList or forceUpdate:
 637            self.iList = self.Listing()
 638
 639        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 640        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 641            fH.write(jsonDump)
 642
 643        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 644
 645        return jsonDump
 646
 647    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 648        """
 649        Show information about one instrument defined by json data and prints it in Markdown format.
 650
 651        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 652
 653        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
 654        :param show: if `True` then also printing information about instrument and its current price.
 655        :return: multilines text in Markdown format with information about one instrument.
 656        """
 657        splitLine = "|                                                             |                                                        |\n"
 658        infoText = ""
 659
 660        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 661            info = [
 662                "# Main information\n\n",
 663                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
 664                "| Parameters                                                  | Values                                                 |\n",
 665                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 666                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 667                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 668            ]
 669
 670            if "sector" in iJSON.keys() and iJSON["sector"]:
 671                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 672
 673            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
 674                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
 675
 676            info.extend([
 677                splitLine,
 678                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 679                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
 680            ])
 681
 682            if "isin" in iJSON.keys() and iJSON["isin"]:
 683                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 684
 685            if "classCode" in iJSON.keys():
 686                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 687
 688            info.extend([
 689                splitLine,
 690                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 691                splitLine,
 692                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 693                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 694                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 695            ])
 696
 697            if iJSON["figi"]:
 698                self._figi = iJSON["figi"]
 699                iJSON = iJSON | self.RequestTradingStatus()
 700
 701                info.extend([
 702                    splitLine,
 703                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 704                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 705                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 706                ])
 707
 708            info.append(splitLine)
 709
 710            if "type" in iJSON.keys() and iJSON["type"]:
 711                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 712
 713                if "shareType" in iJSON.keys() and iJSON["shareType"]:
 714                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
 715
 716            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 717                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 718
 719            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 720                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 721
 722            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 723                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 724
 725            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 726                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 727
 728            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 729                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 730
 731            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 732                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 733
 734            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 735                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 736
 737            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 738                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 739
 740            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 741                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 742
 743            if "currency" in iJSON.keys():
 744                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 745
 746            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 747                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 748
 749            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 750                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 751
 752            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 753                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 754
 755            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 756                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 757
 758            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 759                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 760
 761            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 762                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 763
 764            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 765                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 766
 767            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 768                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 769
 770            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 771                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 772
 773            iExt = None
 774            if iJSON["type"] == "Bonds":
 775                info.extend([
 776                    splitLine,
 777                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 778                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 779                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 780                        iJSON["nominal"]["currency"],
 781                    )),
 782                ])
 783
 784                if "floatingCouponFlag" in iJSON.keys():
 785                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 786
 787                if "amortizationFlag" in iJSON.keys():
 788                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 789
 790                info.append(splitLine)
 791
 792                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 793                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 794
 795                if iJSON["figi"]:
 796                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 797
 798                    info.extend([
 799                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 800                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 801                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 802                    ])
 803
 804                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 805                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 806                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 807                        iJSON["aciValue"]["currency"]
 808                    )))
 809
 810            if "currentPrice" in iJSON.keys():
 811                info.append(splitLine)
 812
 813                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 814                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 815
 816                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 817                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 818                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 819                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 820                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 821
 822                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 823                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 824
 825                info.extend([
 826                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 827                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 828                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 829                    )),
 830                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 831                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 832                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 833                    )),
 834                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 835                        "{:.2f}%{}".format(
 836                            iJSON["currentPrice"]["changes"],
 837                            " ({}{:.2f} {})".format(
 838                                "+" if bondChangesDelta > 0 else "",
 839                                bondChangesDelta,
 840                                aciCurrency
 841                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 842                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 843                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 844                                currency
 845                            ),
 846                        )
 847                    ),
 848                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 849                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 850                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 851                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 852                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 853                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 854                    )),
 855                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 856                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 857                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 858                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 859                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 860                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 861                    )),
 862                ])
 863
 864            if "lot" in iJSON.keys():
 865                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 866
 867            if "step" in iJSON.keys() and iJSON["step"] != 0:
 868                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 869
 870            # Add bond payment calendar:
 871            if iJSON["type"] == "Bonds":
 872                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 873                info.extend(["\n#", strCalendar])
 874
 875            infoText += "".join(info)
 876
 877            if show:
 878                uLogger.info("{}".format(infoText))
 879
 880            else:
 881                uLogger.debug("{}".format(infoText))
 882
 883            if self.infoFile is not None:
 884                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 885                    fH.write(infoText)
 886
 887                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 888
 889                if self.useHTMLReports:
 890                    htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html"
 891                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
 892                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText))
 893
 894                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
 895
 896        return infoText
 897
 898    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 899        """
 900        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 901
 902        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 903        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 904        :return: JSON formatted data with information about instrument.
 905        """
 906        tickerJSON = {}
 907        if self.moreDebug:
 908            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
 909
 910        if not self._ticker:
 911            uLogger.warning("self._ticker variable is not be empty!")
 912
 913        else:
 914            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 915                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
 916                raise Exception("Instrument not allowed")
 917
 918            if not self.iList:
 919                self.iList = self.Listing()
 920
 921            if self._ticker in self.iList["Shares"].keys():
 922                tickerJSON = self.iList["Shares"][self._ticker]
 923                if self.moreDebug:
 924                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
 925
 926            elif self._ticker in self.iList["Currencies"].keys():
 927                tickerJSON = self.iList["Currencies"][self._ticker]
 928                if self.moreDebug:
 929                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
 930
 931            elif self._ticker in self.iList["Bonds"].keys():
 932                tickerJSON = self.iList["Bonds"][self._ticker]
 933                if self.moreDebug:
 934                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
 935
 936            elif self._ticker in self.iList["Etfs"].keys():
 937                tickerJSON = self.iList["Etfs"][self._ticker]
 938                if self.moreDebug:
 939                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
 940
 941            elif self._ticker in self.iList["Futures"].keys():
 942                tickerJSON = self.iList["Futures"][self._ticker]
 943                if self.moreDebug:
 944                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
 945
 946        if tickerJSON:
 947            self._figi = tickerJSON["figi"]
 948
 949            if requestPrice:
 950                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 951
 952                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 953                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 954
 955                else:
 956                    tickerJSON["currentPrice"]["changes"] = 0
 957
 958            if show:
 959                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 960
 961        else:
 962            if show:
 963                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
 964
 965        return tickerJSON
 966
 967    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 968        """
 969        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
 970
 971        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
 972        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 973        :return: JSON formatted data with information about instrument.
 974        """
 975        figiJSON = {}
 976        if self.moreDebug:
 977            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
 978
 979        if not self._figi:
 980            uLogger.warning("self._figi variable is not be empty!")
 981
 982        else:
 983            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
 984                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
 985                raise Exception("Instrument not allowed")
 986
 987            if not self.iList:
 988                self.iList = self.Listing()
 989
 990            for item in self.iList["Shares"].keys():
 991                if self._figi == self.iList["Shares"][item]["figi"]:
 992                    figiJSON = self.iList["Shares"][item]
 993
 994                    if self.moreDebug:
 995                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
 996
 997                    break
 998
 999            if not figiJSON:
1000                for item in self.iList["Currencies"].keys():
1001                    if self._figi == self.iList["Currencies"][item]["figi"]:
1002                        figiJSON = self.iList["Currencies"][item]
1003
1004                        if self.moreDebug:
1005                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
1006
1007                        break
1008
1009            if not figiJSON:
1010                for item in self.iList["Bonds"].keys():
1011                    if self._figi == self.iList["Bonds"][item]["figi"]:
1012                        figiJSON = self.iList["Bonds"][item]
1013
1014                        if self.moreDebug:
1015                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
1016
1017                        break
1018
1019            if not figiJSON:
1020                for item in self.iList["Etfs"].keys():
1021                    if self._figi == self.iList["Etfs"][item]["figi"]:
1022                        figiJSON = self.iList["Etfs"][item]
1023
1024                        if self.moreDebug:
1025                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1026
1027                        break
1028
1029            if not figiJSON:
1030                for item in self.iList["Futures"].keys():
1031                    if self._figi == self.iList["Futures"][item]["figi"]:
1032                        figiJSON = self.iList["Futures"][item]
1033
1034                        if self.moreDebug:
1035                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1036
1037                        break
1038
1039        if figiJSON:
1040            self._figi = figiJSON["figi"]
1041            self._ticker = figiJSON["ticker"]
1042
1043            if requestPrice:
1044                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1045
1046                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1047                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1048
1049                else:
1050                    figiJSON["currentPrice"]["changes"] = 0
1051
1052            if show:
1053                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1054
1055        else:
1056            if show:
1057                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1058
1059        return figiJSON
1060
1061    def GetCurrentPrices(self, show: bool = True) -> dict:
1062        """
1063        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1064        `{"buy": [{"price": 1243.8, "quantity": 193},
1065                  {"price": 1244.0, "quantity": 168},
1066                  {"price": 1244.8, "quantity": 5},
1067                  {"price": 1245.0, "quantity": 61},
1068                  {"price": 1245.4, "quantity": 60}],
1069          "sell": [{"price": 1243.6, "quantity": 8},
1070                   {"price": 1242.6, "quantity": 10},
1071                   {"price": 1242.4, "quantity": 18},
1072                   {"price": 1242.2, "quantity": 50},
1073                   {"price": 1242.0, "quantity": 113}],
1074          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1075        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1076        - sell: list of dicts with Buyers prices,
1077            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1078            - quantity: volume value by current price in lots,
1079        - limitUp: current trade session limit price, maximum,
1080        - limitDown: current trade session limit price, minimum,
1081        - lastPrice: last deal price of the instrument,
1082        - closePrice: previous trade session close price of the instrument.
1083
1084        See also: `SearchByTicker()` and `SearchByFIGI()`.
1085        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1086        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1087
1088        :param show: if `True` then print DOM to log and console.
1089        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1090                 If an error occurred then returns an empty record:
1091                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1092        """
1093        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1094
1095        if self.depth < 1:
1096            uLogger.error("Depth of Market (DOM) must be >=1!")
1097            raise Exception("Incorrect value")
1098
1099        if not (self._ticker or self._figi):
1100            uLogger.error("self._ticker or self._figi variables must be defined!")
1101            raise Exception("Ticker or FIGI required")
1102
1103        if self._ticker and not self._figi:
1104            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1105            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1106
1107        if not self._ticker and self._figi:
1108            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1109            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1110
1111        if not self._figi:
1112            uLogger.error("FIGI is not defined!")
1113            raise Exception("Ticker or FIGI required")
1114
1115        else:
1116            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1117
1118            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1119            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1120            self.body = str({"figi": self._figi, "depth": self.depth})
1121            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1122
1123            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1124                # list of dicts with sellers orders:
1125                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1126
1127                # list of dicts with buyers orders:
1128                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1129
1130                # max price of instrument at this time:
1131                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1132
1133                # min price of instrument at this time:
1134                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1135
1136                # last price of deal with instrument:
1137                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1138
1139                # last close price of instrument:
1140                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1141
1142            else:
1143                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1144                uLogger.debug("Server response: {}".format(pricesResponse))
1145
1146            if show:
1147                if prices["buy"] or prices["sell"]:
1148                    info = [
1149                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1150                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1151                            self._ticker,
1152                            self._figi,
1153                            self.depth,
1154                        ),
1155                        "-" * 60, "\n",
1156                        "             Orders of Buyers | Orders of Sellers\n",
1157                        "-" * 60, "\n",
1158                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1159                        "-" * 60, "\n",
1160                    ]
1161
1162                    if not prices["buy"]:
1163                        info.append("                              | No orders!\n")
1164                        sumBuy = 0
1165
1166                    else:
1167                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1168                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1169                        for item in maxMinSorted:
1170                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1171
1172                    if not prices["sell"]:
1173                        info.append("No orders!                    |\n")
1174                        sumSell = 0
1175
1176                    else:
1177                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1178                        for item in prices["sell"]:
1179                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1180
1181                    info.extend([
1182                        "-" * 60, "\n",
1183                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1184                        "-" * 60, "\n",
1185                    ])
1186
1187                    infoText = "".join(info)
1188
1189                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1190
1191                else:
1192                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1193
1194        return prices
1195
1196    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1197        """
1198        This method get and show information about all available broker instruments for current user account.
1199        If `instrumentsFile` string is not empty then also save information to this file.
1200
1201        :param show: if `True` then print results to console, if `False` — print only to file.
1202        :return: multi-lines string with all available broker instruments
1203        """
1204        if not self.iList:
1205            self.iList = self.Listing()
1206
1207        info = [
1208            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1209            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1210        ]
1211
1212        # add instruments count by type:
1213        for iType in self.iList.keys():
1214            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1215
1216        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1217        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1218
1219        # generating info tables with all instruments by type:
1220        for iType in self.iList.keys():
1221            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1222
1223            for instrument in self.iList[iType].keys():
1224                iName = self.iList[iType][instrument]["name"]  # instrument's name
1225                if len(iName) > 57:
1226                    iName = "{}...".format(iName[:54])  # right trim for a long string
1227
1228                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1229                    self.iList[iType][instrument]["ticker"],
1230                    iName,
1231                    self.iList[iType][instrument]["figi"],
1232                    self.iList[iType][instrument]["currency"],
1233                    self.iList[iType][instrument]["lot"],
1234                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1235                ))
1236
1237        infoText = "".join(info)
1238
1239        if show:
1240            uLogger.info(infoText)
1241
1242        if self.instrumentsFile:
1243            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1244                fH.write(infoText)
1245
1246            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1247
1248            if self.useHTMLReports:
1249                htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html"
1250                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1251                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText))
1252
1253                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1254
1255        return infoText
1256
1257    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1258        """
1259        This method search and show information about instruments by part of its ticker, FIGI or name.
1260        If `searchResultsFile` string is not empty then also save information to this file.
1261
1262        :param pattern: string with part of ticker, FIGI or instrument's name.
1263        :param show: if `True` then print results to console, if `False` — return list of result only.
1264        :return: list of dictionaries with all found instruments.
1265        """
1266        if not self.iList:
1267            self.iList = self.Listing()
1268
1269        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contain only filtered instruments
1270        compiledPattern = re.compile(pattern, re.IGNORECASE)
1271
1272        for iType in self.iList:
1273            for instrument in self.iList[iType].values():
1274                searchResult = compiledPattern.search(" ".join(
1275                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1276                ))
1277
1278                if searchResult:
1279                    searchResults[iType][instrument["ticker"]] = instrument
1280
1281        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1282        info = [
1283            "# Search results\n\n",
1284            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1285            "* **Search pattern:** [{}]\n".format(pattern),
1286            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1287            '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n'
1288        ]
1289        infoShort = info[:]
1290
1291        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1292        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1293        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1294
1295        if resultsLen == 0:
1296            info.append("\nNo results\n")
1297            infoShort.append("\nNo results\n")
1298            uLogger.warning("No results. Try changing your search pattern.")
1299
1300        else:
1301            for iType in searchResults:
1302                iTypeValuesCount = len(searchResults[iType].values())
1303                if iTypeValuesCount > 0:
1304                    info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1305                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1306
1307                    for instrument in searchResults[iType].values():
1308                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1309                            instrument["type"],
1310                            instrument["ticker"],
1311                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1312                            instrument["figi"],
1313                        ))
1314
1315                    if iTypeValuesCount <= 5:
1316                        infoShort.extend(info[-iTypeValuesCount:])
1317
1318                    else:
1319                        infoShort.extend(info[-5:])
1320                        infoShort.append(skippedLine)
1321
1322        infoText = "".join(info)
1323        infoTextShort = "".join(infoShort)
1324
1325        if show:
1326            uLogger.info(infoTextShort)
1327            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1328
1329        if self.searchResultsFile:
1330            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1331                fH.write(infoText)
1332
1333            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1334
1335            if self.useHTMLReports:
1336                htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html"
1337                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1338                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText))
1339
1340                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1341
1342        return searchResults
1343
1344    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1345        """
1346        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1347
1348        :param instruments: list of strings with tickers or FIGIs.
1349        :return: list with unique instrument FIGIs only.
1350        """
1351        requestedInstruments = []
1352        for iName in instruments:
1353            if iName not in self.aliases.keys():
1354                if iName not in requestedInstruments:
1355                    requestedInstruments.append(iName)
1356
1357            else:
1358                if iName not in requestedInstruments:
1359                    if self.aliases[iName] not in requestedInstruments:
1360                        requestedInstruments.append(self.aliases[iName])
1361
1362        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1363
1364        onlyUniqueFIGIs = []
1365        for iName in requestedInstruments:
1366            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1367                continue
1368
1369            self._ticker = iName
1370            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1371
1372            if not iData:
1373                self._ticker = ""
1374                self._figi = iName
1375
1376                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1377
1378                if not iData:
1379                    self._figi = ""
1380                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1381
1382            if iData and iData["figi"] not in onlyUniqueFIGIs:
1383                onlyUniqueFIGIs.append(iData["figi"])
1384
1385        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1386
1387        return onlyUniqueFIGIs
1388
1389    def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]:
1390        """
1391        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1392
1393        See limits: https://tinkoff.github.io/investAPI/limits/
1394
1395        If `pricesFile` string is not empty then also save information to this file.
1396
1397        :param instruments: list of strings with tickers or FIGIs.
1398        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1399        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1400                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1401        """
1402        if instruments is None or not instruments:
1403            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1404            raise Exception("Ticker or FIGI required")
1405
1406        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1407
1408        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1409
1410        iList = []  # trying to get info and current prices about all unique instruments:
1411        for self._figi in onlyUniqueFIGIs:
1412            iData = self.SearchByFIGI(requestPrice=True)
1413            iList.append(iData)
1414
1415        self.ShowListOfPrices(iList, show)
1416
1417        return iList
1418
1419    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1420        """
1421        Show table contains current prices of given instruments.
1422
1423        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1424                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1425        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1426        :return: multilines text in Markdown format as a table contains current prices.
1427        """
1428        infoText = ""
1429
1430        if show or self.pricesFile:
1431            info = [
1432                "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1433                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1434                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1435            ]
1436
1437            for item in iList:
1438                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1439                    item["ticker"],
1440                    item["figi"],
1441                    item["type"],
1442                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1443                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1444                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1445                    "{} / {}".format(
1446                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1447                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1448                    ),
1449                    "{} / {}".format(
1450                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1451                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1452                    ),
1453                    item["currency"],
1454                ))
1455
1456            infoText = "".join(info)
1457
1458            if show:
1459                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1460
1461            if self.pricesFile:
1462                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1463                    fH.write(infoText)
1464
1465                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1466
1467                if self.useHTMLReports:
1468                    htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html"
1469                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1470                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText))
1471
1472                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1473
1474        return infoText
1475
1476    def RequestTradingStatus(self) -> dict:
1477        """
1478        Requesting trading status for the instrument defined by `figi` variable.
1479
1480        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1481
1482        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1483
1484        :return: dictionary with trading status attributes. Response example:
1485                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1486                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1487        """
1488        if self._figi is None or not self._figi:
1489            uLogger.error("Variable `figi` must be defined for using this method!")
1490            raise Exception("FIGI required")
1491
1492        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1493
1494        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1495        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1496        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1497
1498        if self.moreDebug:
1499            uLogger.debug("Records about current trading status successfully received")
1500
1501        return tradingStatus
1502
1503    def RequestPortfolio(self) -> dict:
1504        """
1505        Requesting actual user's portfolio for current `accountId`.
1506
1507        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1508
1509        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1510
1511        :return: dictionary with user's portfolio.
1512        """
1513        if self.accountId is None or not self.accountId:
1514            uLogger.error("Variable `accountId` must be defined for using this method!")
1515            raise Exception("Account ID required")
1516
1517        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1518
1519        self.body = str({"accountId": self.accountId})
1520        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1521        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1522
1523        if self.moreDebug:
1524            uLogger.debug("Records about user's portfolio successfully received")
1525
1526        return rawPortfolio
1527
1528    def RequestPositions(self) -> dict:
1529        """
1530        Requesting open positions by currencies and instruments for current `accountId`.
1531
1532        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1533
1534        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1535
1536        :return: dictionary with open positions by instruments.
1537        """
1538        if self.accountId is None or not self.accountId:
1539            uLogger.error("Variable `accountId` must be defined for using this method!")
1540            raise Exception("Account ID required")
1541
1542        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1543
1544        self.body = str({"accountId": self.accountId})
1545        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1546        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1547
1548        if self.moreDebug:
1549            uLogger.debug("Records about current open positions successfully received")
1550
1551        return rawPositions
1552
1553    def RequestPendingOrders(self) -> list:
1554        """
1555        Requesting current actual pending limit orders for current `accountId`.
1556
1557        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1558
1559        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1560
1561        :return: list of dictionaries with pending limit orders.
1562        """
1563        if self.accountId is None or not self.accountId:
1564            uLogger.error("Variable `accountId` must be defined for using this method!")
1565            raise Exception("Account ID required")
1566
1567        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1568
1569        self.body = str({"accountId": self.accountId})
1570        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1571        rawResponse = self.SendAPIRequest(ordersURL, reqType="POST")
1572
1573        if "orders" in rawResponse.keys():
1574            rawOrders = rawResponse["orders"]
1575            uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1576
1577        else:
1578            rawOrders = []
1579            uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse))
1580
1581        return rawOrders
1582
1583    def RequestStopOrders(self) -> list:
1584        """
1585        Requesting current actual stop orders for current `accountId`.
1586
1587        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1588
1589        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1590
1591        :return: list of dictionaries with stop orders.
1592        """
1593        if self.accountId is None or not self.accountId:
1594            uLogger.error("Variable `accountId` must be defined for using this method!")
1595            raise Exception("Account ID required")
1596
1597        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1598
1599        self.body = str({"accountId": self.accountId})
1600        stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1601        rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST")
1602
1603        if "stopOrders" in rawResponse.keys():
1604            rawStopOrders = rawResponse["stopOrders"]
1605            uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1606
1607        else:
1608            rawStopOrders = []
1609            uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse))
1610
1611        return rawStopOrders
1612
1613    def Overview(self, show: bool = False, details: str = "full") -> dict:
1614        """
1615        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1616        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1617        and `overviewBondsCalendarFile` are defined then also save information to file.
1618
1619        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1620        many requests about the state of the portfolio, and then, based on the received data, a large number
1621        of calculation and statistics are collected.
1622
1623        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1624        :param details: how detailed should the information be?
1625        - `full` — shows full available information about portfolio status (by default),
1626        - `positions` — shows only open positions,
1627        - `orders` — shows only sections of open limits and stop orders.
1628        - `digest` — show a short digest of the portfolio status,
1629        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1630        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1631        :return: dictionary with client's raw portfolio and some statistics.
1632        """
1633        if self.accountId is None or not self.accountId:
1634            uLogger.error("Variable `accountId` must be defined for using this method!")
1635            raise Exception("Account ID required")
1636
1637        view = {
1638            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1639                "headers": {},  # list of dictionaries, response headers without "positions" section
1640                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1641                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1642                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1643                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1644                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1645                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1646                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1647                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1648                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1649            },
1650            "stat": {  # --- some statistics calculated using "raw" sections:
1651                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1652                "availableRUB": 0.,  # available rubles (without other currencies)
1653                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1654                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1655                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1656                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1657                "sharesCostRUB": 0.,  # costs of all shares in RUB
1658                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1659                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1660                "futuresCostRUB": 0.,  # costs of all futures in RUB
1661                "Currencies": [],  # list of dictionaries of all currencies statistics
1662                "Shares": [],  # list of dictionaries of all shares statistics
1663                "Bonds": [],  # list of dictionaries of all bonds statistics
1664                "Etfs": [],  # list of dictionaries of all etfs statistics
1665                "Futures": [],  # list of dictionaries of all futures statistics
1666                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1667                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1668                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1669                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1670                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1671            },
1672            "analytics": {  # --- some analytics of portfolio:
1673                "distrByAssets": {},  # portfolio distribution by assets
1674                "distrByCompanies": {},  # portfolio distribution by companies
1675                "distrBySectors": {},  # portfolio distribution by sectors
1676                "distrByCurrencies": {},  # portfolio distribution by currencies
1677                "distrByCountries": {},  # portfolio distribution by countries
1678                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1679            }
1680        }
1681
1682        details = details.lower()
1683        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1684        if details not in availableDetails:
1685            details = "full"
1686            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1687
1688        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1689
1690        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1691        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1692        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1693        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1694
1695        # save response headers without "positions" section:
1696        for key in portfolioResponse.keys():
1697            if key != "positions":
1698                view["raw"]["headers"][key] = portfolioResponse[key]
1699
1700            else:
1701                continue
1702
1703        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1704        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1705        for item in portfolioResponse["positions"]:
1706            if item["instrumentType"] == "currency":
1707                self._figi = item["figi"]
1708                if not self._figi and item["ticker"]:
1709                    self._ticker = item["ticker"]
1710                    self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1711
1712                curr = self.SearchByFIGI(requestPrice=False)
1713
1714                # current price of currency in RUB:
1715                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1716                    "name": curr["name"],
1717                    "currentPrice": NanoToFloat(
1718                        item["currentPrice"]["units"],
1719                        item["currentPrice"]["nano"]
1720                    ),
1721                }
1722
1723                view["raw"]["Currencies"].append(item)
1724
1725            elif item["instrumentType"] == "share":
1726                view["raw"]["Shares"].append(item)
1727
1728            elif item["instrumentType"] == "bond":
1729                view["raw"]["Bonds"].append(item)
1730
1731            elif item["instrumentType"] == "etf":
1732                view["raw"]["Etfs"].append(item)
1733
1734            elif item["instrumentType"] == "futures":
1735                view["raw"]["Futures"].append(item)
1736
1737            else:
1738                continue
1739
1740        # how many volume of currencies (by ISO currency name) are blocked:
1741        for item in view["raw"]["positions"]["blocked"]:
1742            blocked = NanoToFloat(item["units"], item["nano"])
1743            if blocked > 0:
1744                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1745
1746        # how many volume of instruments (by FIGI) are blocked:
1747        for item in view["raw"]["positions"]["securities"]:
1748            blocked = int(item["blocked"])
1749            if blocked > 0:
1750                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1751
1752        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1753
1754        if "rub" in allBlocked.keys():
1755            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1756
1757        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1758        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1759        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1760        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1761        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1762        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1763        view["stat"]["portfolioCostRUB"] = sum([
1764            view["stat"]["allCurrenciesCostRUB"],
1765            view["stat"]["sharesCostRUB"],
1766            view["stat"]["bondsCostRUB"],
1767            view["stat"]["etfsCostRUB"],
1768            view["stat"]["futuresCostRUB"],
1769        ])
1770
1771        # --- calculating some portfolio statistics:
1772        byComp = {}  # distribution by companies
1773        bySect = {}  # distribution by sectors
1774        byCurr = {}  # distribution by currencies (include RUB)
1775        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1776        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1777
1778        for item in portfolioResponse["positions"]:
1779            self._figi = item["figi"]
1780            if not self._figi and item["ticker"]:
1781                self._ticker = item["ticker"]
1782                self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1783
1784            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1785
1786            if instrument:
1787                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1788                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1789
1790                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1791                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1792
1793                else:
1794                    blocked = 0
1795
1796                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1797                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1798                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1799                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1800                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1801                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1802                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1803                cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1804                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1805                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1806                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1807                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1808
1809                statData = {
1810                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1811                    "ticker": instrument["ticker"],  # ticker by FIGI
1812                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1813                    "volume": volume,  # available volume of instrument
1814                    "lots": lots,  # volume in lots of instrument
1815                    "direction": direction,  # direction of an instrument's position: short or long
1816                    "blocked": blocked,  # blocked volume of currency or instrument
1817                    "currentPrice": curPrice,  # current instrument's price in basic asset
1818                    "average": average,  # current average position price
1819                    "cost": cost,  # current cost of all volume of instrument in basic asset
1820                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1821                    "costRUB": costRUB,  # cost of instrument in ruble
1822                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1823                    "profit": profit,  # expected profit at current moment
1824                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1825                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1826                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1827                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1828                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1829                    "step": instrument["step"],  # minimum price increment
1830                }
1831
1832                # adding distribution by unique countries:
1833                if statData["country"] not in byCountry.keys():
1834                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1835
1836                else:
1837                    byCountry[statData["country"]]["cost"] += costRUB
1838                    byCountry[statData["country"]]["percent"] += percentCostRUB
1839
1840                if item["instrumentType"] != "currency":
1841                    # adding distribution by unique companies:
1842                    if statData["name"]:
1843                        if statData["name"] not in byComp.keys():
1844                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1845
1846                        else:
1847                            byComp[statData["name"]]["cost"] += costRUB
1848                            byComp[statData["name"]]["percent"] += percentCostRUB
1849
1850                    # adding distribution by unique sectors:
1851                    if statData["sector"] not in bySect.keys():
1852                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1853
1854                    else:
1855                        bySect[statData["sector"]]["cost"] += costRUB
1856                        bySect[statData["sector"]]["percent"] += percentCostRUB
1857
1858                # adding distribution by unique currencies:
1859                if currency not in byCurr.keys():
1860                    byCurr[currency] = {
1861                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1862                        "cost": costRUB,
1863                        "percent": percentCostRUB
1864                    }
1865
1866                else:
1867                    byCurr[currency]["cost"] += costRUB
1868                    byCurr[currency]["percent"] += percentCostRUB
1869
1870                # saving statistics for every instrument:
1871                if item["instrumentType"] == "currency":
1872                    view["stat"]["Currencies"].append(statData)
1873
1874                    # update dict with free funds for trading (total - blocked) by currencies
1875                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1876                    view["stat"]["funds"][currency] = {
1877                        "total": volume,
1878                        "totalCostRUB": costRUB,  # total volume cost in rubles
1879                        "free": volume - blocked,
1880                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1881                    }
1882
1883                elif item["instrumentType"] == "share":
1884                    view["stat"]["Shares"].append(statData)
1885
1886                elif item["instrumentType"] == "bond":
1887                    view["stat"]["Bonds"].append(statData)
1888
1889                elif item["instrumentType"] == "etf":
1890                    view["stat"]["Etfs"].append(statData)
1891
1892                elif item["instrumentType"] == "Futures":
1893                    view["stat"]["Futures"].append(statData)
1894
1895                else:
1896                    continue
1897
1898        # total changes in Russian Ruble:
1899        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1900        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1901        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1902        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1903        view["stat"]["funds"]["rub"] = {
1904            "total": view["stat"]["availableRUB"],
1905            "totalCostRUB": view["stat"]["availableRUB"],
1906            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1907            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1908        }
1909
1910        # --- pending limit orders sector data:
1911        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1912        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1913
1914        for item in view["raw"]["orders"]:
1915            self._figi = item["figi"]
1916
1917            if item["figi"] not in uniquePendingOrdersFIGIs:
1918                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1919
1920                uniquePendingOrdersFIGIs.append(item["figi"])
1921                uniquePendingOrders[item["figi"]] = instrument
1922
1923            else:
1924                instrument = uniquePendingOrders[item["figi"]]
1925
1926            if instrument:
1927                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1928                orderType = TKS_ORDER_TYPES[item["orderType"]]
1929                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1930                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1931
1932                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1933                if item["direction"] == "ORDER_DIRECTION_BUY":
1934                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1935
1936                else:
1937                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1938
1939                # requested price for order execution:
1940                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1941
1942                # necessary changes in percent to reach target from current price:
1943                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1944
1945                view["stat"]["orders"].append({
1946                    "orderID": item["orderId"],  # orderId number parameter of current order
1947                    "figi": item["figi"],  # FIGI identification
1948                    "ticker": instrument["ticker"],  # ticker name by FIGI
1949                    "lotsRequested": item["lotsRequested"],  # requested lots value
1950                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1951                    "currentPrice": lastPrice,  # current instrument's price for defined action
1952                    "targetPrice": target,  # requested price for order execution in base currency
1953                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1954                    "percentChanges": changes,  # changes in percent to target from current price
1955                    "currency": item["currency"],  # instrument's currency name
1956                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1957                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1958                    "status": orderState,  # order status from TKS_ORDER_STATES
1959                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1960                })
1961
1962        # --- stop orders sector data:
1963        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1964        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1965
1966        for item in view["raw"]["stopOrders"]:
1967            self._figi = item["figi"]
1968
1969            if item["figi"] not in uniqueStopOrdersFIGIs:
1970                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1971
1972                uniqueStopOrdersFIGIs.append(item["figi"])
1973                uniqueStopOrders[item["figi"]] = instrument
1974
1975            else:
1976                instrument = uniqueStopOrders[item["figi"]]
1977
1978            if instrument:
1979                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1980                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1981                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1982
1983                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1984                if "expirationTime" in item.keys():
1985                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1986                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1987
1988                else:
1989                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1990                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1991
1992                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1993                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1994                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1995
1996                else:
1997                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1998
1999                # requested price when stop-order executed:
2000                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
2001
2002                # price for limit-order, set up when stop-order executed:
2003                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
2004
2005                # necessary changes in percent to reach target from current price:
2006                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
2007
2008                view["stat"]["stopOrders"].append({
2009                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2010                    "figi": item["figi"],  # FIGI identification
2011                    "ticker": instrument["ticker"],  # ticker name by FIGI
2012                    "lotsRequested": item["lotsRequested"],  # requested lots value
2013                    "currentPrice": lastPrice,  # current instrument's price for defined action
2014                    "targetPrice": target,  # requested price for stop-order execution in base currency
2015                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2016                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2017                    "percentChanges": changes,  # changes in percent to target from current price
2018                    "currency": item["currency"],  # instrument's currency name
2019                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2020                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2021                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2022                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2023                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2024                })
2025
2026        # --- calculating data for analytics section:
2027        # portfolio distribution by assets:
2028        view["analytics"]["distrByAssets"] = {
2029            "Ruble": {
2030                "uniques": 1,
2031                "cost": view["stat"]["availableRUB"],
2032                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2033            },
2034            "Currencies": {
2035                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2036                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2037                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2038            },
2039            "Shares": {
2040                "uniques": len(view["stat"]["Shares"]),
2041                "cost": view["stat"]["sharesCostRUB"],
2042                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2043            },
2044            "Bonds": {
2045                "uniques": len(view["stat"]["Bonds"]),
2046                "cost": view["stat"]["bondsCostRUB"],
2047                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2048            },
2049            "Etfs": {
2050                "uniques": len(view["stat"]["Etfs"]),
2051                "cost": view["stat"]["etfsCostRUB"],
2052                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2053            },
2054            "Futures": {
2055                "uniques": len(view["stat"]["Futures"]),
2056                "cost": view["stat"]["futuresCostRUB"],
2057                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2058            },
2059        }
2060
2061        # portfolio distribution by companies:
2062        view["analytics"]["distrByCompanies"]["All money cash"] = {
2063            "ticker": "",
2064            "cost": view["stat"]["allCurrenciesCostRUB"],
2065            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2066        }
2067        view["analytics"]["distrByCompanies"].update(byComp)
2068
2069        # portfolio distribution by sectors:
2070        view["analytics"]["distrBySectors"]["All money cash"] = {
2071            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2072            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2073        }
2074        view["analytics"]["distrBySectors"].update(bySect)
2075
2076        # portfolio distribution by currencies:
2077        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2078            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2079
2080            if self.moreDebug:
2081                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2082
2083        view["analytics"]["distrByCurrencies"].update(byCurr)
2084        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2085        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2086
2087        # portfolio distribution by countries:
2088        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2089            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2090
2091            if self.moreDebug:
2092                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2093
2094        view["analytics"]["distrByCountries"].update(byCountry)
2095        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2096        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2097
2098        # --- Prepare text statistics overview in human-readable:
2099        if show:
2100            actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)
2101
2102            # Whatever the value `details`, header not changes:
2103            info = [
2104                "# Client's portfolio\n\n",
2105                "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2106                "* **Account ID:** [{}]\n".format(self.accountId),
2107            ]
2108
2109            if details in ["full", "positions", "digest"]:
2110                info.extend([
2111                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2112                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2113                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2114                        view["stat"]["totalChangesRUB"],
2115                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2116                        view["stat"]["totalChangesPercentRUB"],
2117                    ),
2118                ])
2119
2120            if details in ["full", "positions"]:
2121                info.extend([
2122                    "## Open positions\n\n",
2123                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2124                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2125                    "| **Ruble:**                  | {:>31} |          |              |              |                     |                              |\n".format(
2126                        "{:.2f} ({:.2f}) rub".format(
2127                            view["stat"]["availableRUB"],
2128                            view["stat"]["blockedRUB"],
2129                        )
2130                    )
2131                ])
2132
2133                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2134                    return [
2135                        "|                             |                                 |          |              |              |                     |                              |\n",
2136                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2137                            noTradeStr if noTradeStr else typeStr,
2138                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2139                        ),
2140                    ]
2141
2142                def _InfoStr(data: dict, isCurr: bool = False) -> str:
2143                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2144                        "{} [{}]".format(data["ticker"], data["figi"]),
2145                        "{:.2f} ({:.2f}) {}".format(
2146                            data["volume"],
2147                            data["blocked"],
2148                            data["currency"],
2149                        ) if isCurr else "{:.0f} ({:.0f})".format(
2150                            data["volume"],
2151                            data["blocked"],
2152                        ),
2153                        "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."),
2154                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2155                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2156                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2157                        "{}{:.2f} {} ({}{:.2f}%)".format(
2158                            "+" if data["profit"] > 0 else "",
2159                            data["profit"], data["baseCurrencyName"],
2160                            "+" if data["percentProfit"] > 0 else "",
2161                            data["percentProfit"],
2162                        ),
2163                    )
2164
2165                # --- Show currencies section:
2166                if view["stat"]["Currencies"]:
2167                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2168                    for item in view["stat"]["Currencies"]:
2169                        info.append(_InfoStr(item, isCurr=True))
2170
2171                else:
2172                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2173
2174                # --- Show shares section:
2175                if view["stat"]["Shares"]:
2176                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2177
2178                    for item in view["stat"]["Shares"]:
2179                        info.append(_InfoStr(item))
2180
2181                else:
2182                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2183
2184                # --- Show bonds section:
2185                if view["stat"]["Bonds"]:
2186                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2187
2188                    for item in view["stat"]["Bonds"]:
2189                        info.append(_InfoStr(item))
2190
2191                else:
2192                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2193
2194                # --- Show etfs section:
2195                if view["stat"]["Etfs"]:
2196                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2197
2198                    for item in view["stat"]["Etfs"]:
2199                        info.append(_InfoStr(item))
2200
2201                else:
2202                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2203
2204                # --- Show futures section:
2205                if view["stat"]["Futures"]:
2206                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2207
2208                    for item in view["stat"]["Futures"]:
2209                        info.append(_InfoStr(item))
2210
2211                else:
2212                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2213
2214            if details in ["full", "orders"]:
2215                # --- Show pending limit orders section:
2216                if view["stat"]["orders"]:
2217                    info.extend([
2218                        "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])),
2219                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2220                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2221                    ])
2222
2223                    for item in view["stat"]["orders"]:
2224                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2225                            "{} [{}]".format(item["ticker"], item["figi"]),
2226                            item["orderID"],
2227                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2228                            "{} {} ({}{:.2f}%)".format(
2229                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2230                                item["baseCurrencyName"],
2231                                "+" if item["percentChanges"] > 0 else "",
2232                                float(item["percentChanges"]),
2233                            ),
2234                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2235                            item["action"],
2236                            item["type"],
2237                            item["date"],
2238                        ))
2239
2240                else:
2241                    info.append("\n## Total pending limit-orders: [0]\n")
2242
2243                # --- Show stop orders section:
2244                if view["stat"]["stopOrders"]:
2245                    info.extend([
2246                        "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])),
2247                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2248                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2249                    ])
2250
2251                    for item in view["stat"]["stopOrders"]:
2252                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2253                            "{} [{}]".format(item["ticker"], item["figi"]),
2254                            item["orderID"],
2255                            item["lotsRequested"],
2256                            "{} {} ({}{:.2f}%)".format(
2257                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2258                                item["baseCurrencyName"],
2259                                "+" if item["percentChanges"] > 0 else "",
2260                                float(item["percentChanges"]),
2261                            ),
2262                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2263                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2264                            item["action"],
2265                            item["type"],
2266                            item["expType"],
2267                            item["createDate"],
2268                            item["expDate"],
2269                        ))
2270
2271                else:
2272                    info.append("\n## Total stop-orders: [0]\n")
2273
2274            if details in ["full", "analytics"]:
2275                # -- Show analytics section:
2276                if view["stat"]["portfolioCostRUB"] > 0:
2277                    info.extend([
2278                        "\n# Analytics\n\n"
2279                        "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2280                        "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2281                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2282                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2283                            view["stat"]["totalChangesRUB"],
2284                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2285                            view["stat"]["totalChangesPercentRUB"],
2286                        ),
2287                        "\n## Portfolio distribution by assets\n"
2288                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2289                        "|------------------------------------|---------|---------|--------------------|\n",
2290                    ])
2291
2292                    for key in view["analytics"]["distrByAssets"].keys():
2293                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2294                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2295                                key,
2296                                view["analytics"]["distrByAssets"][key]["uniques"],
2297                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2298                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2299                            ))
2300
2301                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2302
2303                    info.extend([
2304                        "\n## Portfolio distribution by companies\n"
2305                        "\n| Company                                      | Percent | Current cost       |\n",
2306                        aSepLine,
2307                    ])
2308
2309                    for company in view["analytics"]["distrByCompanies"].keys():
2310                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2311                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2312                                "{}{}".format(
2313                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2314                                    company,
2315                                ),
2316                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2317                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2318                            ))
2319
2320                    info.extend([
2321                        "\n## Portfolio distribution by sectors\n"
2322                        "\n| Sector                                       | Percent | Current cost       |\n",
2323                        aSepLine,
2324                    ])
2325
2326                    for sector in view["analytics"]["distrBySectors"].keys():
2327                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2328                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2329                                sector,
2330                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2331                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2332                            ))
2333
2334                    info.extend([
2335                        "\n## Portfolio distribution by currencies\n"
2336                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2337                        aSepLine,
2338                    ])
2339
2340                    for curr in view["analytics"]["distrByCurrencies"].keys():
2341                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2342                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2343                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2344                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2345                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2346                            ))
2347
2348                    info.extend([
2349                        "\n## Portfolio distribution by countries\n"
2350                        "\n| Assets by country                            | Percent | Current cost       |\n",
2351                        aSepLine,
2352                    ])
2353
2354                    for country in view["analytics"]["distrByCountries"].keys():
2355                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2356                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2357                                country,
2358                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2359                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2360                            ))
2361
2362            if details in ["full", "calendar"]:
2363                # -- Show bonds payment calendar section:
2364                if view["stat"]["Bonds"]:
2365                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2366                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2367                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2368
2369                else:
2370                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2371
2372            infoText = "".join(info)
2373
2374            uLogger.info(infoText)
2375
2376            if details == "full" and self.overviewFile:
2377                filename = self.overviewFile
2378
2379            elif details == "digest" and self.overviewDigestFile:
2380                filename = self.overviewDigestFile
2381
2382            elif details == "positions" and self.overviewPositionsFile:
2383                filename = self.overviewPositionsFile
2384
2385            elif details == "orders" and self.overviewOrdersFile:
2386                filename = self.overviewOrdersFile
2387
2388            elif details == "analytics" and self.overviewAnalyticsFile:
2389                filename = self.overviewAnalyticsFile
2390
2391            elif details == "calendar" and self.overviewBondsCalendarFile:
2392                filename = self.overviewBondsCalendarFile
2393
2394            else:
2395                filename = ""
2396
2397            if filename:
2398                with open(filename, "w", encoding="UTF-8") as fH:
2399                    fH.write(infoText)
2400
2401                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2402
2403                if self.useHTMLReports:
2404                    htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html"
2405                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2406                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText))
2407
2408                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2409
2410        return view
2411
2412    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2413        """
2414        Returns history operations between two given dates for current `accountId`.
2415        If `reportFile` string is not empty then also save human-readable report.
2416        Shows some statistical data of closed positions.
2417
2418        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2419        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2420        :param show: if `True` then also prints all records to the console.
2421        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2422        :return: original list of dictionaries with history of deals records from API ("operations" key):
2423                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2424                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2425        """
2426        if self.accountId is None or not self.accountId:
2427            uLogger.error("Variable `accountId` must be defined for using this method!")
2428            raise Exception("Account ID required")
2429
2430        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2431
2432        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2433
2434        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2435        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2436        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2437        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2438        customStat = {}  # custom statistics in additional to responseJSON
2439
2440        # --- output report in human-readable format:
2441        if show or self.reportFile:
2442            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2443            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2444            nextDay = ""
2445
2446            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2447
2448            if len(ops) > 0:
2449                customStat = {
2450                    "opsCount": 0,  # total operations count
2451                    "buyCount": 0,  # buy operations
2452                    "sellCount": 0,  # sell operations
2453                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2454                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2455                    "payIn": {"rub": 0.},  # Deposit brokerage account
2456                    "payOut": {"rub": 0.},  # Withdrawals
2457                    "divs": {"rub": 0.},  # Dividends income
2458                    "coupons": {"rub": 0.},  # Coupon's income
2459                    "brokerCom": {"rub": 0.},  # Service commissions
2460                    "serviceCom": {"rub": 0.},  # Service commissions
2461                    "marginCom": {"rub": 0.},  # Margin commissions
2462                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2463                }
2464
2465                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2466                for item in ops:
2467                    if item["state"] == "OPERATION_STATE_EXECUTED":
2468                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2469
2470                        # count buy operations:
2471                        if "_BUY" in item["operationType"]:
2472                            customStat["buyCount"] += 1
2473
2474                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2475                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2476
2477                            else:
2478                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2479
2480                        # count sell operations:
2481                        elif "_SELL" in item["operationType"]:
2482                            customStat["sellCount"] += 1
2483
2484                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2485                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2486
2487                            else:
2488                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2489
2490                        # count incoming operations:
2491                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2492                            if item["payment"]["currency"] in customStat["payIn"].keys():
2493                                customStat["payIn"][item["payment"]["currency"]] += payment
2494
2495                            else:
2496                                customStat["payIn"][item["payment"]["currency"]] = payment
2497
2498                        # count withdrawals operations:
2499                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2500                            if item["payment"]["currency"] in customStat["payOut"].keys():
2501                                customStat["payOut"][item["payment"]["currency"]] += payment
2502
2503                            else:
2504                                customStat["payOut"][item["payment"]["currency"]] = payment
2505
2506                        # count dividends income:
2507                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2508                            if item["payment"]["currency"] in customStat["divs"].keys():
2509                                customStat["divs"][item["payment"]["currency"]] += payment
2510
2511                            else:
2512                                customStat["divs"][item["payment"]["currency"]] = payment
2513
2514                        # count coupon's income:
2515                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2516                            if item["payment"]["currency"] in customStat["coupons"].keys():
2517                                customStat["coupons"][item["payment"]["currency"]] += payment
2518
2519                            else:
2520                                customStat["coupons"][item["payment"]["currency"]] = payment
2521
2522                        # count broker commissions:
2523                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2524                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2525                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2526
2527                            else:
2528                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2529
2530                        # count service commissions:
2531                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2532                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2533                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2534
2535                            else:
2536                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2537
2538                        # count margin commissions:
2539                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2540                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2541                                customStat["marginCom"][item["payment"]["currency"]] += payment
2542
2543                            else:
2544                                customStat["marginCom"][item["payment"]["currency"]] = payment
2545
2546                        # count withholding taxes:
2547                        elif "_TAX" in item["operationType"]:
2548                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2549                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2550
2551                            else:
2552                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2553
2554                        else:
2555                            continue
2556
2557                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2558
2559                # --- view "Actions" lines:
2560                info.extend([
2561                    "| Report sections            |                               |                              |                      |                        |\n",
2562                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2563                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2564                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2565                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2566                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2567                    ),
2568                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2569                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2570                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2571                    ),
2572                ])
2573
2574                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2575                for key in opsKeys:
2576                    if key == "rub":
2577                        continue
2578
2579                    info.extend([
2580                        "|                            |                               | {:<28} |                      |                        |\n".format(
2581                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2582                        ),
2583                        "|                            |                               | {:<28} |                      |                        |\n".format(
2584                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2585                        ),
2586                    ])
2587
2588                info.append(splitLine1)
2589
2590                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2591                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2592                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2593                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2594                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2595                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2596                    )
2597
2598                # --- view "Payments" lines:
2599                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2600                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2601
2602                for key in paymentsKeys:
2603                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2604
2605                info.append(splitLine1)
2606
2607                # --- view "Commissions and taxes" lines:
2608                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2609                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2610
2611                for key in comKeys:
2612                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2613
2614                info.extend([
2615                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2616                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2617                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2618                ])
2619
2620            else:
2621                info.append("Broker returned no operations during this period\n")
2622
2623            # --- view "Operations" section:
2624            for item in ops:
2625                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2626                    continue
2627
2628                else:
2629                    self._figi = item["figi"]
2630                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2631                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2632
2633                    # group of deals during one day:
2634                    if nextDay and item["date"].split("T")[0] != nextDay:
2635                        info.append(splitLine2)
2636                        nextDay = ""
2637
2638                    else:
2639                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2640
2641                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2642                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2643                        self._figi if self._figi else "—",
2644                        instrument["ticker"] if instrument else "—",
2645                        instrument["type"] if instrument else "—",
2646                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2647                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2648                        TKS_OPERATION_STATES[item["state"]],
2649                        TKS_OPERATION_TYPES[item["operationType"]],
2650                    ))
2651
2652            infoText = "".join(info)
2653
2654            if show:
2655                if self.moreDebug:
2656                    uLogger.debug("Records about history of a client's operations successfully received")
2657
2658                uLogger.info(infoText)
2659
2660            if self.reportFile:
2661                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2662                    fH.write(infoText)
2663
2664                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2665
2666                if self.useHTMLReports:
2667                    htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html"
2668                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2669                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText))
2670
2671                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2672
2673        return ops, customStat
2674
2675    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2676        """
2677        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2678
2679        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2680        Warning! Broker server used ISO UTC time by default.
2681
2682        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2683        Also, `historyFile` used to update history with `onlyMissing` parameter.
2684
2685        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2686
2687        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2688        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2689        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2690                         `"hour"`, `"day"`. Default: `"hour"`.
2691        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2692                            False by default. Warning! History appends only from last candle to current time
2693                            with always update last candle!
2694        :param csvSep: separator if csv-file is used, `,` by default.
2695        :param show: if `True` then also prints Pandas DataFrame to the console.
2696        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2697                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2698        """
2699        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2700        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2701        history = None  # empty pandas object for history
2702
2703        if interval not in TKS_CANDLE_INTERVALS.keys():
2704            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2705            raise Exception("Incorrect value")
2706
2707        if not (self._ticker or self._figi):
2708            uLogger.error("Ticker or FIGI must be defined!")
2709            raise Exception("Ticker or FIGI required")
2710
2711        if self._ticker and not self._figi:
2712            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2713            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2714
2715        if self._figi and not self._ticker:
2716            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2717            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2718
2719        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2720        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2721        if interval.lower() != "day":
2722            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2723
2724        delta = dtEnd - dtStart  # current UTC time minus last time in file
2725        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2726
2727        # calculate history length in candles:
2728        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2729        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2730            length += 1  # to avoid fraction time
2731
2732        # calculate data blocks count:
2733        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2734
2735        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2736        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2737        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2738        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2739        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2740
2741        tempOld = None  # pandas object for old history, if --only-missing key present
2742        lastTime = None  # datetime object of last old candle in file
2743
2744        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2745            uLogger.debug("--only-missing key present, add only last missing candles...")
2746            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2747
2748            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2749
2750            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2751            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2752            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2753            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2754
2755            # get last datetime object from last string in file or minus 1 delta if file is empty:
2756            if len(tempOld) > 0:
2757                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2758
2759            else:
2760                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2761
2762            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2763
2764        responseJSONs = []  # raw history blocks of data
2765
2766        blockEnd = dtEnd
2767        for item in range(blocks):
2768            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2769            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2770
2771            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2772                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2773            ))
2774
2775            if blockStart == blockEnd:
2776                uLogger.debug("Skipped this zero-length block...")
2777
2778            else:
2779                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2780                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2781                self.body = str({
2782                    "figi": self._figi,
2783                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2784                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2785                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2786                })
2787                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2788
2789                if "code" in responseJSON.keys():
2790                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2791
2792                else:
2793                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2794                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2795
2796                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2797
2798            blockEnd = blockStart
2799
2800        printCount = len(responseJSONs)  # candles to show in console
2801        if responseJSONs:
2802            tempHistory = pd.DataFrame(
2803                data={
2804                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2805                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2806                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2807                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2808                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2809                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2810                    "volume": [int(item["volume"]) for item in responseJSONs],
2811                },
2812                index=range(len(responseJSONs)),
2813                columns=["date", "time", "open", "high", "low", "close", "volume"],
2814            )
2815            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2816            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2817
2818            # append only newest candles to old history if --only-missing key present:
2819            if onlyMissing and tempOld is not None and lastTime is not None:
2820                index = 0  # find start index in tempHistory data:
2821
2822                for i, item in tempHistory.iterrows():
2823                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2824
2825                    if curTime == lastTime:
2826                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2827                        index = i
2828                        printCount = index + 1
2829                        break
2830
2831                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2832
2833            else:
2834                history = tempHistory  # if no `--only-missing` key then load full data from server
2835
2836            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2837
2838        if history is not None and not history.empty:
2839            if show:
2840                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2841                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2842                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2843                ))
2844
2845        else:
2846            uLogger.warning("Received an empty candles history!")
2847
2848        if self.historyFile is not None:
2849            if history is not None and not history.empty:
2850                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2851                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2852
2853            else:
2854                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2855
2856        else:
2857            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2858
2859        return history
2860
2861    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2862        """
2863        Load candles history from csv-file and return Pandas DataFrame object.
2864
2865        See also: `History()` and `ShowHistoryChart()` methods.
2866
2867        :param filePath: path to csv-file to open.
2868        """
2869        loadedHistory = None  # init candles data object
2870
2871        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2872
2873        if os.path.exists(filePath):
2874            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2875
2876            tfStr = self.priceModel.FormattedDelta(
2877                self.priceModel.timeframe,
2878                "{days} days {hours}h {minutes}m {seconds}s",
2879            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2880                self.priceModel.timeframe,
2881                "{hours}h {minutes}m {seconds}s",
2882            )
2883
2884            if loadedHistory is not None and not loadedHistory.empty:
2885                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2886                    len(loadedHistory),
2887                    tfStr,
2888                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2889                )
2890
2891            else:
2892                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2893
2894        else:
2895            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2896
2897        return loadedHistory
2898
2899    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2900        """
2901        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2902
2903        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2904        Default: `index.html` (both for interact and non-interact candlesticks chart).
2905
2906        See also: `History()` and `LoadHistory()` methods.
2907
2908        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2909        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2910                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2911                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2912                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2913        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2914                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2915        """
2916        if isinstance(candles, str):
2917            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2918            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2919
2920        elif isinstance(candles, pd.DataFrame):
2921            self.priceModel.prices = candles  # set candles chain from variable
2922            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2923
2924            if "datetime" not in candles.columns:
2925                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2926
2927        else:
2928            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2929            raise Exception("Incorrect value")
2930
2931        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2932
2933        if interact:
2934            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2935
2936            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2937
2938        else:
2939            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2940
2941            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2942
2943        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2944
2945    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2946        """
2947        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2948        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2949
2950        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2951
2952        :param operation: string "Buy" or "Sell".
2953        :param lots: volume, integer count of lots >= 1.
2954        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2955        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2956        :param expDate: string "Undefined" by default or local date in future,
2957                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2958        :return: JSON with response from broker server.
2959        """
2960        if self.accountId is None or not self.accountId:
2961            uLogger.error("Variable `accountId` must be defined for using this method!")
2962            raise Exception("Account ID required")
2963
2964        if operation is None or not operation or operation not in ("Buy", "Sell"):
2965            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2966            raise Exception("Incorrect value")
2967
2968        if lots is None or lots < 1:
2969            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2970            lots = 1
2971
2972        if tp is None or tp < 0:
2973            tp = 0
2974
2975        if sl is None or sl < 0:
2976            sl = 0
2977
2978        if expDate is None or not expDate:
2979            expDate = "Undefined"
2980
2981        if not (self._ticker or self._figi):
2982            uLogger.error("Ticker or FIGI must be defined!")
2983            raise Exception("Ticker or FIGI required")
2984
2985        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
2986        self._ticker = instrument["ticker"]
2987        self._figi = instrument["figi"]
2988
2989        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
2990
2991        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2992        self.body = str({
2993            "figi": self._figi,
2994            "quantity": str(lots),
2995            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2996            "accountId": str(self.accountId),
2997            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2998        })
2999        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
3000
3001        if "orderId" in response.keys():
3002            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
3003                operation, response["orderId"],
3004                self._ticker, self._figi, lots,
3005                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
3006                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
3007                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
3008            ))
3009
3010            if tp > 0:
3011                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
3012
3013            if sl > 0:
3014                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3015
3016        else:
3017            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
3018
3019        return response
3020
3021    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3022        """
3023        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3024        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3025
3026        See also: `Order()` and `Trade()` docstrings.
3027
3028        :param lots: volume, integer count of lots >= 1.
3029        :param tp: float > 0, take profit price of stop-order.
3030        :param sl: float > 0, stop loss price of stop-order.
3031        :param expDate: it's a local date in future.
3032                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3033        :return: JSON with response from broker server.
3034        """
3035        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
3036
3037    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3038        """
3039        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3040        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3041
3042        See also: `Order()` and `Trade()` docstrings.
3043
3044        :param lots: volume, integer count of lots >= 1.
3045        :param tp: float > 0, take profit price of stop-order.
3046        :param sl: float > 0, stop loss price of stop-order.
3047        :param expDate: it's a local date in the future.
3048                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3049        :return: JSON with response from broker server.
3050        """
3051        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
3052
3053    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3054        """
3055        Close position of given instruments.
3056
3057        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3058        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3059                         This avoids unnecessary downloading data from the server.
3060        """
3061        if instruments is None or not instruments:
3062            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3063            raise Exception("Ticker or FIGI required")
3064
3065        if isinstance(instruments, str):
3066            instruments = [instruments]
3067
3068        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3069        if uniqueInstruments:
3070            if portfolio is None or not portfolio:
3071                portfolio = self.Overview(show=False)
3072
3073            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3074            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3075
3076            for self._figi in uniqueInstruments:
3077                if self._figi not in allOpened:
3078                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3079                    continue
3080
3081                # search open trade info about instrument by ticker:
3082                instrument = {}
3083                for iType in TKS_INSTRUMENTS:
3084                    if instrument:
3085                        break
3086
3087                    for item in portfolio["stat"][iType]:
3088                        if item["figi"] == self._figi:
3089                            instrument = item
3090                            break
3091
3092                if instrument:
3093                    self._ticker = instrument["ticker"]
3094                    self._figi = instrument["figi"]
3095
3096                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3097                        self._ticker,
3098                        self._figi,
3099                        int(instrument["volume"]),
3100                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3101                    ))
3102
3103                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3104
3105                    if tradeLots > 0:
3106                        if instrument["blocked"] > 0:
3107                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3108                                instrument["blocked"],
3109                                self._ticker,
3110                                tradeLots,
3111                            ))
3112
3113                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3114                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3115
3116                    else:
3117                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
3118
3119    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3120        """
3121        Close all positions of given instruments with defined type.
3122
3123        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3124        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3125                         This avoids unnecessary downloading data from the server.
3126        """
3127        if iType not in TKS_INSTRUMENTS:
3128            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3129
3130        else:
3131            if portfolio is None or not portfolio:
3132                portfolio = self.Overview(show=False)
3133
3134            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3135            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3136
3137            if tickers and portfolio:
3138                self.CloseTrades(tickers, portfolio)
3139
3140            else:
3141                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3142
3143    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3144        """
3145        Universal method to create market or limit orders with all available parameters for current `accountId`.
3146        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3147
3148        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3149        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3150
3151        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3152        then broker immediately open market order as you can do simple --buy or --sell operations!
3153
3154        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3155        When current price will go up or down to target price value then broker opens a limit order.
3156        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3157
3158        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3159
3160        :param operation: string "Buy" or "Sell".
3161        :param orderType: string "Limit" or "Stop".
3162        :param lots: volume, integer count of lots >= 1.
3163        :param targetPrice: target price > 0. This is open trade price for limit order.
3164        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3165                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3166        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3167                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3168                         Stop loss order always executed by market price.
3169        :param expDate: string "Undefined" by default or local date in future.
3170                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3171                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3172                        A limit order has no expiration date, it lasts until the end of the trading day.
3173        :return: JSON with response from broker server.
3174        """
3175        if self.accountId is None or not self.accountId:
3176            uLogger.error("Variable `accountId` must be defined for using this method!")
3177            raise Exception("Account ID required")
3178
3179        if operation is None or not operation or operation not in ("Buy", "Sell"):
3180            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3181            raise Exception("Incorrect value")
3182
3183        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3184            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3185            raise Exception("Incorrect value")
3186
3187        if lots is None or lots < 1:
3188            uLogger.error("You must define trade volume > 0: integer count of lots!")
3189            raise Exception("Incorrect value")
3190
3191        if targetPrice is None or targetPrice <= 0:
3192            uLogger.error("Target price for limit-order must be greater than 0!")
3193            raise Exception("Incorrect value")
3194
3195        if limitPrice is None or limitPrice <= 0:
3196            limitPrice = targetPrice
3197
3198        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3199            stopType = "Limit"
3200
3201        if expDate is None or not expDate:
3202            expDate = "Undefined"
3203
3204        if not (self._ticker or self._figi):
3205            uLogger.error("Tocker or FIGI must be defined!")
3206            raise Exception("Ticker or FIGI required")
3207
3208        response = {}
3209        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3210        self._ticker = instrument["ticker"]
3211        self._figi = instrument["figi"]
3212
3213        if orderType == "Limit":
3214            uLogger.debug(
3215                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3216                    self._ticker, self._figi,
3217                    operation, lots, targetPrice, instrument["currency"],
3218                ))
3219
3220            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3221            self.body = str({
3222                "figi": self._figi,
3223                "quantity": str(lots),
3224                "price": FloatToNano(targetPrice),
3225                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3226                "accountId": str(self.accountId),
3227                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3228            })
3229            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3230
3231            if "orderId" in response.keys():
3232                uLogger.info(
3233                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format(
3234                        response["orderId"], self._ticker, self._figi, operation, lots,
3235                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3236                    ))
3237
3238                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3239                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3240                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3241                            targetPrice, instrument["currency"],
3242                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3243                        ))
3244
3245                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3246                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3247                            targetPrice, instrument["currency"],
3248                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3249                        ))
3250
3251            else:
3252                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3253
3254        if orderType == "Stop":
3255            uLogger.debug(
3256                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3257                    self._ticker, self._figi,
3258                    operation, lots,
3259                    targetPrice, instrument["currency"],
3260                    limitPrice, instrument["currency"],
3261                    stopType, expDate,
3262                ))
3263
3264            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3265            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3266            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3267
3268            body = {
3269                "figi": self._figi,
3270                "quantity": str(lots),
3271                "price": FloatToNano(limitPrice),
3272                "stopPrice": FloatToNano(targetPrice),
3273                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3274                "accountId": str(self.accountId),
3275                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3276                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3277            }
3278
3279            if expDateUTC:
3280                body["expireDate"] = expDateUTC
3281
3282            self.body = str(body)
3283            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3284
3285            if "stopOrderId" in response.keys():
3286                uLogger.info(
3287                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format(
3288                        response["stopOrderId"], self._ticker, self._figi, operation, lots,
3289                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3290                        "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"],
3291                        TKS_STOP_ORDER_TYPES[stopOrderType],
3292                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3293                    ))
3294
3295                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3296                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3297                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3298                            targetPrice, instrument["currency"],
3299                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3300                        ))
3301
3302                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3303                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3304                            targetPrice, instrument["currency"],
3305                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3306                        ))
3307
3308            else:
3309                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3310
3311        return response
3312
3313    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3314        """
3315        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3316        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3317        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3318        See also: `Order()` docstring.
3319
3320        :param lots: volume, integer count of lots >= 1.
3321        :param targetPrice: target price > 0. This is open trade price for limit order.
3322        :return: JSON with response from broker server.
3323        """
3324        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3325
3326    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3327        """
3328        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3329        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3330        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3331        target price value then broker opens a limit order. See also: `Order()` docstring.
3332
3333        :param lots: volume, integer count of lots >= 1.
3334        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3335        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3336                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3337        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3338                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3339        :param expDate: string "Undefined" by default or local date in future.
3340                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3341                        This date is converting to UTC format for server.
3342        :return: JSON with response from broker server.
3343        """
3344        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3345
3346    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3347        """
3348        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3349        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3350        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3351        See also: `Order()` docstring.
3352
3353        :param lots: volume, integer count of lots >= 1.
3354        :param targetPrice: target price > 0. This is open trade price for limit order.
3355        :return: JSON with response from broker server.
3356        """
3357        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3358
3359    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3360        """
3361        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3362        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3363        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3364        target price value then broker opens a limit order. See also: `Order()` docstring.
3365
3366        :param lots: volume, integer count of lots >= 1.
3367        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3368        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3369                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3370        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3371                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3372        :param expDate: string "Undefined" by default or local date in future.
3373                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3374                        This date is converting to UTC format for server.
3375        :return: JSON with response from broker server.
3376        """
3377        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3378
3379    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3380        """
3381        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3382
3383        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3384        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3385                             This avoids unnecessary downloading data from the server.
3386        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3387        """
3388        if self.accountId is None or not self.accountId:
3389            uLogger.error("Variable `accountId` must be defined for using this method!")
3390            raise Exception("Account ID required")
3391
3392        if orderIDs:
3393            if allOrdersIDs is None:
3394                rawOrders = self.RequestPendingOrders()
3395                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3396
3397            if allStopOrdersIDs is None:
3398                rawStopOrders = self.RequestStopOrders()
3399                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3400
3401            for orderID in orderIDs:
3402                idInPendingOrders = orderID in allOrdersIDs
3403                idInStopOrders = orderID in allStopOrdersIDs
3404
3405                if not (idInPendingOrders or idInStopOrders):
3406                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3407                    continue
3408
3409                else:
3410                    if idInPendingOrders:
3411                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3412
3413                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3414                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3415                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3416                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3417
3418                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3419                            if self.moreDebug:
3420                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3421
3422                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3423
3424                        else:
3425                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3426
3427                    elif idInStopOrders:
3428                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3429
3430                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3431                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3432                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3433                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3434
3435                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3436                            if self.moreDebug:
3437                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3438
3439                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3440
3441                        else:
3442                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3443
3444                    else:
3445                        continue
3446
3447    def CloseAllOrders(self) -> None:
3448        """
3449        Gets a list of open pending and stop orders and cancel it all.
3450        """
3451        rawOrders = self.RequestPendingOrders()
3452        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3453        lenOrders = len(allOrdersIDs)
3454
3455        rawStopOrders = self.RequestStopOrders()
3456        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3457        lenSOrders = len(allStopOrdersIDs)
3458
3459        if lenOrders > 0 or lenSOrders > 0:
3460            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3461
3462            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3463
3464        else:
3465            uLogger.info("Orders not found, nothing to cancel.")
3466
3467    def CloseAll(self, *args) -> None:
3468        """
3469        Close all available (not blocked) opened trades and orders.
3470
3471        Also, you can select one or more keywords case-insensitive:
3472        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3473
3474        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3475        """
3476        overview = self.Overview(show=False)  # get all open trades info
3477
3478        if len(args) == 0:
3479            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3480            self.CloseAllOrders()  # close all pending and stop orders
3481
3482            for iType in TKS_INSTRUMENTS:
3483                if iType != "Currencies":
3484                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3485
3486        else:
3487            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3488            lowerArgs = [x.lower() for x in args]
3489
3490            if "orders" in lowerArgs:
3491                self.CloseAllOrders()  # close all pending and stop orders
3492
3493            for iType in TKS_INSTRUMENTS:
3494                if iType.lower() in lowerArgs and iType != "Currencies":
3495                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3496
3497    def CloseAllByTicker(self, instrument: str) -> None:
3498        """
3499        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3500
3501        This method searches opened trade and orders of instrument throw all portfolio and then use
3502        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3503
3504        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3505
3506        :param instrument: string with ticker.
3507        """
3508        if instrument is None or not instrument:
3509            uLogger.error("Ticker name must be defined for using this method!")
3510            raise Exception("Ticker required")
3511
3512        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3513
3514        self._ticker = instrument  # try to set instrument as ticker
3515        self._figi = ""
3516
3517        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3518        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3519
3520        if limitAll and self.IsInLimitOrders(portfolio=overview):
3521            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3522            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3523
3524        if stopAll and self.IsInStopOrders(portfolio=overview):
3525            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3526            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3527
3528        if self.IsInPortfolio(portfolio=overview):
3529            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3530            self.CloseTrades(instruments=[instrument], portfolio=overview)
3531
3532    def CloseAllByFIGI(self, instrument: str) -> None:
3533        """
3534        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3535
3536        This method searches opened trade and orders of instrument throw all portfolio and then use
3537        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3538
3539        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3540
3541        :param instrument: string with FIGI id.
3542        """
3543        if instrument is None or not instrument:
3544            uLogger.error("FIGI id must be defined for using this method!")
3545            raise Exception("FIGI required")
3546
3547        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3548
3549        self._ticker = ""
3550        self._figi = instrument  # try to set instrument as FIGI id
3551
3552        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3553        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3554
3555        if limitAll and self.IsInLimitOrders(portfolio=overview):
3556            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3557            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3558
3559        if stopAll and self.IsInStopOrders(portfolio=overview):
3560            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3561            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3562
3563        if self.IsInPortfolio(portfolio=overview):
3564            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3565            self.CloseTrades(instruments=[instrument], portfolio=overview)
3566
3567    @staticmethod
3568    def ParseOrderParameters(operation, **inputParameters):
3569        """
3570        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3571
3572        :param operation: string "Buy" or "Sell".
3573        :param inputParameters: this is dict of strings that looks like this
3574               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3575               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3576               "prices" key: one or more prices to open limit-orders
3577               Counts of values in lots and prices lists must be equals!
3578        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3579        """
3580        # TODO: update order grid work with api v2
3581        pass
3582        # uLogger.debug("Input parameters: {}".format(inputParameters))
3583        #
3584        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3585        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3586        #     raise Exception("Incorrect value")
3587        #
3588        # if "l" in inputParameters.keys():
3589        #     inputParameters["lots"] = inputParameters.pop("l")
3590        #
3591        # if "p" in inputParameters.keys():
3592        #     inputParameters["prices"] = inputParameters.pop("p")
3593        #
3594        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3595        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3596        #     raise Exception("Incorrect value")
3597        #
3598        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3599        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3600        #
3601        # if len(lots) != len(prices):
3602        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3603        #     raise Exception("Incorrect value")
3604        #
3605        # uLogger.debug("Extracted parameters for orders:")
3606        # uLogger.debug("lots = {}".format(lots))
3607        # uLogger.debug("prices = {}".format(prices))
3608        #
3609        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3610        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3611        # uLogger.debug("Order parameters: {}".format(result))
3612        #
3613        # return result
3614
3615    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3616        """
3617        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3618
3619        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3620        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3621        """
3622        result = False
3623        msg = "Instrument not defined!"
3624
3625        if portfolio is None or not portfolio:
3626            portfolio = self.Overview(show=False)
3627
3628        if self._ticker:
3629            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3630            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3631
3632            for iType in TKS_INSTRUMENTS:
3633                for instrument in portfolio["stat"][iType]:
3634                    if instrument["ticker"] == self._ticker:
3635                        result = True
3636                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3637                        break
3638
3639        elif self._figi:
3640            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3641            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3642
3643            for iType in TKS_INSTRUMENTS:
3644                for instrument in portfolio["stat"][iType]:
3645                    if instrument["figi"] == self._figi:
3646                        result = True
3647                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3648                        break
3649
3650        else:
3651            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3652
3653        uLogger.debug(msg)
3654
3655        return result
3656
3657    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3658        """
3659        Returns instrument from the user's portfolio if it presents there.
3660        Instrument must be defined by `ticker` (highly priority) or `figi`.
3661
3662        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3663        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3664        """
3665        result = None
3666        msg = "Instrument not defined!"
3667
3668        if portfolio is None or not portfolio:
3669            portfolio = self.Overview(show=False)
3670
3671        if self._ticker:
3672            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3673            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3674
3675            for iType in TKS_INSTRUMENTS:
3676                for instrument in portfolio["stat"][iType]:
3677                    if instrument["ticker"] == self._ticker:
3678                        result = instrument
3679                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3680                        break
3681
3682        elif self._figi:
3683            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3684            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3685
3686            for iType in TKS_INSTRUMENTS:
3687                for instrument in portfolio["stat"][iType]:
3688                    if instrument["figi"] == self._figi:
3689                        result = instrument
3690                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3691                        break
3692
3693        else:
3694            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3695
3696        uLogger.debug(msg)
3697
3698        return result
3699
3700    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3701        """
3702        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3703
3704        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3705
3706        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3707        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3708        """
3709        result = False
3710        msg = "Instrument not defined!"
3711
3712        if portfolio is None or not portfolio:
3713            portfolio = self.Overview(show=False)
3714
3715        if self._ticker:
3716            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3717            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3718
3719            for instrument in portfolio["stat"]["orders"]:
3720                if instrument["ticker"] == self._ticker:
3721                    result = True
3722                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3723                    break
3724
3725        elif self._figi:
3726            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3727            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3728
3729            for instrument in portfolio["stat"]["orders"]:
3730                if instrument["figi"] == self._figi:
3731                    result = True
3732                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3733                    break
3734
3735        else:
3736            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3737
3738        uLogger.debug(msg)
3739
3740        return result
3741
3742    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3743        """
3744        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3745        Instrument must be defined by `ticker` (highly priority) or `figi`.
3746
3747        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3748
3749        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3750        :return: list with `orderID`s of limit orders.
3751        """
3752        result = []
3753        msg = "Instrument not defined!"
3754
3755        if portfolio is None or not portfolio:
3756            portfolio = self.Overview(show=False)
3757
3758        if self._ticker:
3759            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3760            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3761
3762            for instrument in portfolio["stat"]["orders"]:
3763                if instrument["ticker"] == self._ticker:
3764                    result.append(instrument["orderID"])
3765
3766            if result:
3767                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3768
3769        elif self._figi:
3770            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3771            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3772
3773            for instrument in portfolio["stat"]["orders"]:
3774                if instrument["figi"] == self._figi:
3775                    result.append(instrument["orderID"])
3776
3777            if result:
3778                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3779
3780        else:
3781            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3782
3783        uLogger.debug(msg)
3784
3785        return result
3786
3787    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3788        """
3789        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3790
3791        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3792
3793        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3794        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3795        """
3796        result = False
3797        msg = "Instrument not defined!"
3798
3799        if portfolio is None or not portfolio:
3800            portfolio = self.Overview(show=False)
3801
3802        if self._ticker:
3803            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3804            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3805
3806            for instrument in portfolio["stat"]["stopOrders"]:
3807                if instrument["ticker"] == self._ticker:
3808                    result = True
3809                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3810                    break
3811
3812        elif self._figi:
3813            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3814            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3815
3816            for instrument in portfolio["stat"]["stopOrders"]:
3817                if instrument["figi"] == self._figi:
3818                    result = True
3819                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3820                    break
3821
3822        else:
3823            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3824
3825        uLogger.debug(msg)
3826
3827        return result
3828
3829    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3830        """
3831        Returns list with all `orderID`s of opened stop orders for the instrument.
3832        Instrument must be defined by `ticker` (highly priority) or `figi`.
3833
3834        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3835
3836        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3837        :return: list with `orderID`s of stop orders.
3838        """
3839        result = []
3840        msg = "Instrument not defined!"
3841
3842        if portfolio is None or not portfolio:
3843            portfolio = self.Overview(show=False)
3844
3845        if self._ticker:
3846            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3847            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3848
3849            for instrument in portfolio["stat"]["stopOrders"]:
3850                if instrument["ticker"] == self._ticker:
3851                    result.append(instrument["orderID"])
3852
3853            if result:
3854                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3855
3856        elif self._figi:
3857            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3858            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3859
3860            for instrument in portfolio["stat"]["stopOrders"]:
3861                if instrument["figi"] == self._figi:
3862                    result.append(instrument["orderID"])
3863
3864            if result:
3865                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3866
3867        else:
3868            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3869
3870        uLogger.debug(msg)
3871
3872        return result
3873
3874    def RequestLimits(self) -> dict:
3875        """
3876        Method for obtaining the available funds for withdrawal for current `accountId`.
3877
3878        See also:
3879        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3880        - `OverviewLimits()` method
3881
3882        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3883                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3884                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3885                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3886        """
3887        if self.accountId is None or not self.accountId:
3888            uLogger.error("Variable `accountId` must be defined for using this method!")
3889            raise Exception("Account ID required")
3890
3891        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3892
3893        self.body = str({"accountId": self.accountId})
3894        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3895        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3896
3897        if self.moreDebug:
3898            uLogger.debug("Records about available funds for withdrawal successfully received")
3899
3900        return rawLimits
3901
3902    def OverviewLimits(self, show: bool = False) -> dict:
3903        """
3904        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3905
3906        See also: `RequestLimits()`.
3907
3908        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3909        :return: dict with raw parsed data from server and some calculated statistics about it.
3910        """
3911        if self.accountId is None or not self.accountId:
3912            uLogger.error("Variable `accountId` must be defined for using this method!")
3913            raise Exception("Account ID required")
3914
3915        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3916
3917        view = {
3918            "rawLimits": rawLimits,
3919            "limits": {  # parsed data for every currency:
3920                "money": {  # this is an array of portfolio currency positions
3921                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3922                },
3923                "blocked": {  # this is an array of blocked currency
3924                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3925                },
3926                "blockedGuarantee": {  # this is locked money under collateral for futures
3927                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3928                },
3929            },
3930        }
3931
3932        # --- Prepare text table with limits in human-readable format:
3933        if show:
3934            info = [
3935                "# Withdrawal limits\n\n",
3936                "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3937                "* **Account ID:** [{}]\n".format(self.accountId),
3938            ]
3939
3940            if view["limits"]["money"]:
3941                info.extend([
3942                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3943                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3944                ])
3945
3946            else:
3947                info.append("\nNo withdrawal limits\n")
3948
3949            for curr in view["limits"]["money"].keys():
3950                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3951                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3952                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3953
3954                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3955                    "[{}]".format(curr),
3956                    "{:.2f}".format(view["limits"]["money"][curr]),
3957                    "{:.2f}".format(availableMoney),
3958                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3959                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3960                )
3961
3962                if curr == "rub":
3963                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3964
3965                else:
3966                    info.append(infoStr)
3967
3968            infoText = "".join(info)
3969
3970            uLogger.info(infoText)
3971
3972            if self.withdrawalLimitsFile:
3973                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3974                    fH.write(infoText)
3975
3976                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3977
3978                if self.useHTMLReports:
3979                    htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html"
3980                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
3981                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText))
3982
3983                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
3984
3985        return view
3986
3987    def RequestAccounts(self) -> dict:
3988        """
3989        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3990
3991        See also:
3992        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3993        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3994        - `OverviewUserInfo()` method
3995
3996        :return: dict with raw data from server that contains accounts info. Example of dict:
3997                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3998                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3999                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
4000                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
4001        """
4002        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
4003
4004        self.body = str({})
4005        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
4006        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
4007
4008        if self.moreDebug:
4009            uLogger.debug("Records about available accounts successfully received")
4010
4011        return rawAccounts
4012
4013    def RequestUserInfo(self) -> dict:
4014        """
4015        Method for requesting common user's information.
4016
4017        See also:
4018        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
4019        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
4020        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
4021        - `OverviewUserInfo()` method
4022
4023        :return: dict with raw data from server that contains user's information. Example of dict:
4024                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
4025                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
4026        """
4027        uLogger.debug("Requesting common user's information. Wait, please...")
4028
4029        self.body = str({})
4030        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
4031        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
4032
4033        if self.moreDebug:
4034            uLogger.debug("Records about current user successfully received")
4035
4036        return rawUserInfo
4037
4038    def RequestMarginStatus(self, accountId: str = None) -> dict:
4039        """
4040        Method for requesting margin calculation for defined account ID.
4041
4042        See also:
4043        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
4044        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
4045        - `OverviewUserInfo()` method
4046
4047        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
4048        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
4049                 Example of responses:
4050                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
4051                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
4052                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
4053                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
4054                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
4055                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
4056        """
4057        if accountId is None or not accountId:
4058            if self.accountId is None or not self.accountId:
4059                uLogger.error("Variable `accountId` must be defined for using this method!")
4060                raise Exception("Account ID required")
4061
4062            else:
4063                accountId = self.accountId  # use `self.accountId` (main ID) by default
4064
4065        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
4066
4067        self.body = str({"accountId": accountId})
4068        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
4069        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
4070
4071        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
4072            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
4073            rawMargin = {}
4074
4075        else:
4076            if self.moreDebug:
4077                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
4078
4079        return rawMargin
4080
4081    def RequestTariffLimits(self) -> dict:
4082        """
4083        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4084
4085        See also:
4086        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4087        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4088        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4089        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4090        - `OverviewUserInfo()` method
4091
4092        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4093                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4094                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4095        """
4096        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4097
4098        self.body = str({})
4099        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4100        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4101
4102        if self.moreDebug:
4103            uLogger.debug("Records with limits of current tariff successfully received")
4104
4105        return rawTariffLimits
4106
4107    def RequestBondCoupons(self, iJSON: dict) -> dict:
4108        """
4109        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4110        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4111        All dates are in UTC timezone.
4112
4113        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4114        Documentation:
4115        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4116        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4117
4118        See also: `ExtendBondsData()`.
4119
4120        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4121                      If raw iJSON is not data of bond then server returns an error [400] with message:
4122                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4123        :return: dictionary with bond payment calendar. Response example
4124                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4125                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4126                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4127                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4128        """
4129        if iJSON["figi"] is None or not iJSON["figi"]:
4130            uLogger.error("FIGI must be defined for using this method!")
4131            raise Exception("FIGI required")
4132
4133        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4134        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4135
4136        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4137            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4138            self._figi,
4139            startDate,
4140            endDate,
4141        ))
4142
4143        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4144        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4145        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4146
4147        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4148            uLogger.warning("Instrument type is not bond!")
4149
4150        else:
4151            if self.moreDebug:
4152                uLogger.debug("Records about bond payment calendar successfully received")
4153
4154        return calendar
4155
4156    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4157        """
4158        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4159        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4160        coupon yields, current yields and some statistics etc.
4161
4162        WARNING! This is too long operation if a lot of bonds requested from broker server.
4163
4164        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4165
4166        :param instruments: list of strings with tickers or FIGIs.
4167        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4168                     for further used by data scientists or stock analytics.
4169        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4170                 In XLSX-file and Pandas DataFrame fields mean:
4171                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4172                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4173        """
4174        if instruments is None or not instruments:
4175            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4176            raise Exception("Ticker or FIGI required")
4177
4178        if isinstance(instruments, str):
4179            instruments = [instruments]
4180
4181        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4182
4183        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4184
4185        iCount = len(uniqueInstruments)
4186        tooLong = iCount >= 20
4187        if tooLong:
4188            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4189
4190        bonds = None
4191        for i, self._figi in enumerate(uniqueInstruments):
4192            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4193
4194            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4195                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4196                rawBond = self.SearchByFIGI(requestPrice=True)
4197
4198                # Widen raw data with UTC current time (iData["actualDateTime"]):
4199                actualDate = datetime.now(tzutc())
4200                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4201
4202                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4203                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4204
4205                # Replace some values with human-readable:
4206                iData["nominalCurrency"] = iData["nominal"]["currency"]
4207                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4208                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4209                iData["aciCurrency"] = iData["aciValue"]["currency"]
4210                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4211                iData["issueSize"] = int(iData["issueSize"])
4212                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4213                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4214                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4215                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4216                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4217                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4218                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4219                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4220                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4221                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4222
4223                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4224                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4225                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4226                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4227                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4228                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4229                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4230                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4231                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4232                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4233                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4234
4235                # Widen raw data with calendar data from `rawCalendar` values:
4236                calendarData = []
4237                if "events" in iData["rawCalendar"].keys():
4238                    for item in iData["rawCalendar"]["events"]:
4239                        calendarData.append({
4240                            "couponDate": item["couponDate"],
4241                            "couponNumber": int(item["couponNumber"]),
4242                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4243                            "payCurrency": item["payOneBond"]["currency"],
4244                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4245                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4246                            "couponStartDate": item["couponStartDate"],
4247                            "couponEndDate": item["couponEndDate"],
4248                            "couponPeriod": item["couponPeriod"],
4249                        })
4250
4251                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4252                    if "maturityDate" not in iData.keys():
4253                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4254
4255                # Widen raw data with Coupon Rate.
4256                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4257                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4258                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4259                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4260
4261                # Widen raw data with Yield to Maturity (YTM) on current date.
4262                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4263                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4264                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4265                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4266                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4267                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4268
4269                iData["calendar"] = calendarData  # adds calendar at the end
4270
4271                # Remove not used data:
4272                iData.pop("uid")
4273                iData.pop("positionUid")
4274                iData.pop("currentPrice")
4275                iData.pop("rawCalendar")
4276
4277                colNames = list(iData.keys())
4278                if bonds is None:
4279                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4280
4281                else:
4282                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4283
4284            else:
4285                uLogger.warning("Instrument is not a bond!")
4286
4287            processed = round(100 * (i + 1) / iCount, 1)
4288            if tooLong and processed % 5 == 0:
4289                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4290
4291            else:
4292                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4293
4294        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4295
4296        # Saving bonds from Pandas DataFrame to XLSX sheet:
4297        if xlsx and self.bondsXLSXFile:
4298            with pd.ExcelWriter(
4299                    path=self.bondsXLSXFile,
4300                    date_format=TKS_DATE_FORMAT,
4301                    datetime_format=TKS_DATE_TIME_FORMAT,
4302                    mode="w",
4303            ) as writer:
4304                bonds.to_excel(
4305                    writer,
4306                    sheet_name="Extended bonds data",
4307                    index=True,
4308                    encoding="UTF-8",
4309                    freeze_panes=(1, 1),
4310                )  # saving as XLSX-file with freeze first row and column as headers
4311
4312            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4313
4314        return bonds
4315
4316    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4317        """
4318        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4319
4320        WARNING! This is too long operation if a lot of bonds requested from broker server.
4321
4322        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4323
4324        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4325                        extended information about bonds: main info, current prices, bond payment calendar,
4326                        coupon yields, current yields and some statistics etc.
4327                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4328        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4329                     for further used by data scientists or stock analytics.
4330        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4331        """
4332        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4333            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4334
4335        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4336
4337        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4338        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4339        calendar = None
4340        for bond in extBonds.iterrows():
4341            for item in bond[1]["calendar"]:
4342                cData = {
4343                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4344                    "couponDate": item["couponDate"],
4345                    "figi": bond[1]["figi"],
4346                    "ticker": bond[1]["ticker"],
4347                    "name": bond[1]["name"],
4348                    "couponNumber": item["couponNumber"],
4349                    "payOneBond": item["payOneBond"],
4350                    "payCurrency": item["payCurrency"],
4351                    "couponType": item["couponType"],
4352                    "couponPeriod": item["couponPeriod"],
4353                    "fixDate": item["fixDate"],
4354                    "couponStartDate": item["couponStartDate"],
4355                    "couponEndDate": item["couponEndDate"],
4356                }
4357
4358                if calendar is None:
4359                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4360
4361                else:
4362                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4363
4364        if calendar is not None:
4365            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4366
4367            # Saving calendar from Pandas DataFrame to XLSX sheet:
4368            if xlsx:
4369                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4370
4371                with pd.ExcelWriter(
4372                        path=xlsxCalendarFile,
4373                        date_format=TKS_DATE_FORMAT,
4374                        datetime_format=TKS_DATE_TIME_FORMAT,
4375                        mode="w",
4376                ) as writer:
4377                    humanReadable = calendar.copy(deep=True)
4378                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4379                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4380                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4381                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4382                    humanReadable.columns = colNames  # human-readable column names
4383
4384                    humanReadable.to_excel(
4385                        writer,
4386                        sheet_name="Bond payments calendar",
4387                        index=False,
4388                        encoding="UTF-8",
4389                        freeze_panes=(1, 2),
4390                    )  # saving as XLSX-file with freeze first row and column as headers
4391
4392                    del humanReadable  # release df in memory
4393
4394                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4395
4396        return calendar
4397
4398    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4399        """
4400        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4401        Also, creates Markdown file with calendar data, `calendar.md` by default.
4402
4403        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4404
4405        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4406                        extended information about bonds: main info, current prices, bond payment calendar,
4407                        coupon yields, current yields and some statistics etc.
4408                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4409        :param show: if `True` then also printing bonds payment calendar to the console,
4410                     otherwise save to file `calendarFile` only. `False` by default.
4411        :return: multilines text in Markdown format with bonds payment calendar as a table.
4412        """
4413        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4414            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4415
4416        infoText = "# Bond payments calendar\n\n"
4417
4418        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4419
4420        if not (calendar is None or calendar.empty):
4421            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4422
4423            info = [
4424                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4425                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4426                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4427            ]
4428
4429            newMonth = False
4430            notOneBond = calendar["figi"].nunique() > 1
4431            for i, bond in enumerate(calendar.iterrows()):
4432                if newMonth and notOneBond:
4433                    info.append(splitLine)
4434
4435                info.append(
4436                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4437                        "  √" if bond[1]["paid"] else "  —",
4438                        bond[1]["couponDate"].split("T")[0],
4439                        bond[1]["figi"],
4440                        bond[1]["ticker"],
4441                        bond[1]["couponNumber"],
4442                        "{} {}".format(
4443                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4444                            bond[1]["payCurrency"],
4445                        ),
4446                        bond[1]["couponType"],
4447                        bond[1]["couponPeriod"],
4448                        bond[1]["fixDate"].split("T")[0],
4449                    )
4450                )
4451
4452                if i < len(calendar.values) - 1:
4453                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4454                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4455                    newMonth = False if curDate.month == nextDate.month else True
4456
4457                else:
4458                    newMonth = False
4459
4460            infoText += "".join(info)
4461
4462            if show:
4463                uLogger.info("{}".format(infoText))
4464
4465            if self.calendarFile is not None:
4466                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4467                    fH.write(infoText)
4468
4469                uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4470
4471                if self.useHTMLReports:
4472                    htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html"
4473                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4474                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText))
4475
4476                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4477
4478        else:
4479            infoText += "No data\n"
4480
4481        return infoText
4482
4483    def OverviewAccounts(self, show: bool = False) -> dict:
4484        """
4485        Method for parsing and show simple table with all available user accounts.
4486
4487        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4488
4489        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4490        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4491                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4492                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4493                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4494                                                        "closed": "—", "access": "Full access" }, ...}}`
4495        """
4496        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4497
4498        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4499        accounts = {
4500            item["id"]: {
4501                "type": TKS_ACCOUNT_TYPES[item["type"]],
4502                "name": item["name"],
4503                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4504                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4505                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4506                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4507            } for item in rawAccounts["accounts"]
4508        }
4509
4510        # Raw and parsed data with some fields replaced in "stat" section:
4511        view = {
4512            "rawAccounts": rawAccounts,
4513            "stat": accounts,
4514        }
4515
4516        # --- Prepare simple text table with only accounts data in human-readable format:
4517        if show:
4518            info = [
4519                "# User accounts\n\n",
4520                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4521                "| Account ID   | Type                      | Status                    | Name                           |\n",
4522                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4523            ]
4524
4525            for account in view["stat"].keys():
4526                info.extend([
4527                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4528                        account,
4529                        view["stat"][account]["type"],
4530                        view["stat"][account]["status"],
4531                        view["stat"][account]["name"],
4532                    )
4533                ])
4534
4535            infoText = "".join(info)
4536
4537            uLogger.info(infoText)
4538
4539            if self.userAccountsFile:
4540                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4541                    fH.write(infoText)
4542
4543                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4544
4545                if self.useHTMLReports:
4546                    htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html"
4547                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4548                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText))
4549
4550                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4551
4552        return view
4553
4554    def OverviewUserInfo(self, show: bool = False) -> dict:
4555        """
4556        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4557
4558        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4559
4560        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4561        :return: dict with raw parsed data from server and some calculated statistics about it.
4562        """
4563        overview = self.Overview(show=False)  # Request current user portfolio for the ability to calculate missing funds
4564        tmpTicker = self._ticker
4565        self._ticker = "RUB000UTSTOM"  # This instrument show in rub how much money cost current margin
4566        missing = self.GetInstrumentFromPortfolio(portfolio=overview)
4567        self._ticker = tmpTicker
4568
4569        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4570        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4571        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4572        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4573        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4574        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4575
4576        # This is dict with parsed common user data:
4577        userInfo = {
4578            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4579            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4580            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4581            "tariff": rawUserInfo["tariff"],
4582        }
4583
4584        # This is an array of dict with parsed margin statuses for every account IDs:
4585        margins = {}
4586        for accountId in accounts.keys():
4587            if rawMargins[accountId]:
4588                margins[accountId] = {
4589                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4590                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4591                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4592                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4593                    "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4594                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4595                    "missing": missing["volume"],
4596                }
4597
4598            else:
4599                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4600
4601        unary = {}  # unary-connection limits
4602        for item in rawTariffLimits["unaryLimits"]:
4603            if item["limitPerMinute"] in unary.keys():
4604                unary[item["limitPerMinute"]].extend(item["methods"])
4605
4606            else:
4607                unary[item["limitPerMinute"]] = item["methods"]
4608
4609        stream = {}  # stream-connection limits
4610        for item in rawTariffLimits["streamLimits"]:
4611            if item["limit"] in stream.keys():
4612                stream[item["limit"]].extend(item["streams"])
4613
4614            else:
4615                stream[item["limit"]] = item["streams"]
4616
4617        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4618        limits = {
4619            "unary": unary,
4620            "stream": stream,
4621        }
4622
4623        # Raw and parsed data as an output result:
4624        view = {
4625            "rawUserInfo": rawUserInfo,
4626            "rawAccounts": rawAccounts,
4627            "rawMargins": rawMargins,
4628            "rawTariffLimits": rawTariffLimits,
4629            "stat": {
4630                "overview": overview,
4631                "userInfo": userInfo,
4632                "accounts": accounts,
4633                "margins": margins,
4634                "limits": limits,
4635            },
4636        }
4637
4638        # --- Prepare text table with user information in human-readable format:
4639        if show:
4640            info = [
4641                "# Full user information\n\n",
4642                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4643                "## Common information\n\n",
4644                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4645                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4646                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4647                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4648                "\n## User accounts\n\n",
4649            ]
4650
4651            for account in view["stat"]["accounts"].keys():
4652                info.extend([
4653                    "### ID: [{}]\n\n".format(account),
4654                    "| Parameters           | Values                                                       |\n",
4655                    "|----------------------|--------------------------------------------------------------|\n",
4656                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4657                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4658                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4659                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4660                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4661                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4662                ])
4663
4664                if margins[account]:
4665                    info.extend([
4666                        "| Margin status:       | Enabled                                                      |\n",
4667                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4668                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4669                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4670                        "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])),
4671                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4672                        "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])),
4673                    ])
4674
4675                else:
4676                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4677
4678            info.extend([
4679                "\n## Current user tariff limits\n",
4680                "\n### See also\n",
4681                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4682                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4683                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4684                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4685                "\n### Unary limits\n",
4686            ])
4687
4688            if unary:
4689                for key, values in sorted(unary.items()):
4690                    info.append("\n* Max requests per minute: {}\n".format(key))
4691
4692                    for value in values:
4693                        info.append("  - {}\n".format(value))
4694
4695            else:
4696                info.append("\nNot available\n")
4697
4698            info.append("\n### Stream limits\n")
4699
4700            if stream:
4701                for key, values in sorted(stream.items()):
4702                    info.append("\n* Max stream connections: {}\n".format(key))
4703
4704                    for value in values:
4705                        info.append("  - {}\n".format(value))
4706
4707            else:
4708                info.append("\nNot available\n")
4709
4710            infoText = "".join(info)
4711
4712            uLogger.info(infoText)
4713
4714            if self.userInfoFile:
4715                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4716                    fH.write(infoText)
4717
4718                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4719
4720                if self.useHTMLReports:
4721                    htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html"
4722                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4723                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText))
4724
4725                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4726
4727        return view

This class implements methods to work with Tinkoff broker server.

Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/

About token: https://tinkoff.github.io/investAPI/token/

TinkoffBrokerServer( token: str, accountId: str = None, useCache: bool = True, defaultCache: str = 'dump.json')
 86    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 87        """
 88        Main class init.
 89
 90        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 91        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 92                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 93        :param useCache: use default cache file with raw data to use instead of `iList`.
 94                         True by default. Cache is auto-update if new day has come.
 95                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 96        :param defaultCache: path to default cache file. `dump.json` by default.
 97        """
 98        if token is None or not token:
 99            try:
100                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
101                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
102
103            except KeyError:
104                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
105                raise Exception("Token required")
106
107        else:
108            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
109            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
110
111        if accountId is None or not accountId:
112            try:
113                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
114                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
115
116            except KeyError:
117                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
118
119        else:
120            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
121            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
122
123        self.version = __version__  # duplicate here used TKSBrokerAPI main version
124        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
125
126        Latest version: https://pypi.org/project/tksbrokerapi/
127        """
128
129        self.__lock = Lock()  # initialize multiprocessing mutex lock
130
131        self.aliases = TKS_TICKER_ALIASES
132        """Some aliases instead official tickers.
133
134        See also: `TKSEnums.TKS_TICKER_ALIASES`
135        """
136
137        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
138
139        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
140
141        self._ticker = ""
142        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
143
144        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
145        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
146
147        See also: `SearchByTicker()`, `SearchInstruments()`.
148        """
149
150        self._figi = ""
151        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
152
153        See also: `SearchByFIGI()`, `SearchInstruments()`.
154        """
155
156        self.depth = 1
157        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
158
159        See also: `GetCurrentPrices()`.
160        """
161
162        self.server = r"https://invest-public-api.tinkoff.ru/rest"
163        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
164
165        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
166        """
167
168        uLogger.debug("Broker API server: {}".format(self.server))
169
170        self.timeout = 15
171        """Server operations timeout in seconds. Default: `15`.
172
173        See also: `SendAPIRequest()`.
174        """
175
176        self.headers = {
177            "Content-Type": "application/json",
178            "accept": "application/json",
179            "Authorization": "Bearer {}".format(self.token),
180            "x-app-name": "Tim55667757.TKSBrokerAPI",
181        }
182        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
183
184        See also: `SendAPIRequest()`.
185        """
186
187        self.body = None
188        """Request body which send to broker server. Default: `None`.
189
190        See also: `SendAPIRequest()`.
191        """
192
193        self.moreDebug = False
194        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
195
196        self.useHTMLReports = False
197        """
198        If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default.
199        
200        See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
201        """
202
203        self.historyFile = None
204        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
205
206        See also: `History()`.
207        """
208
209        self.htmlHistoryFile = "index.html"
210        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
211
212        See also: `ShowHistoryChart()`.
213        """
214
215        self.instrumentsFile = "instruments.md"
216        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
217
218        See also: `ShowInstrumentsInfo()`.
219        """
220
221        self.searchResultsFile = "search-results.md"
222        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
223
224        See also: `SearchInstruments()`.
225        """
226
227        self.pricesFile = "prices.md"
228        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
229
230        See also: `GetListOfPrices()`.
231        """
232
233        self.infoFile = "info.md"
234        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
235
236        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
237        """
238
239        self.bondsXLSXFile = "ext-bonds.xlsx"
240        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
241        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
242
243        See also: `ExtendBondsData()`.
244        """
245
246        self.calendarFile = "calendar.md"
247        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
248        
249        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
250
251        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
252        """
253
254        self.overviewFile = "overview.md"
255        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
256
257        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
258        """
259
260        self.overviewDigestFile = "overview-digest.md"
261        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
262
263        See also: `Overview()` with parameter `details="digest"`.
264        """
265
266        self.overviewPositionsFile = "overview-positions.md"
267        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
268
269        See also: `Overview()` with parameter `details="positions"`.
270        """
271
272        self.overviewOrdersFile = "overview-orders.md"
273        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
274
275        See also: `Overview()` with parameter `details="orders"`.
276        """
277
278        self.overviewAnalyticsFile = "overview-analytics.md"
279        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
280
281        See also: `Overview()` with parameter `details="analytics"`.
282        """
283
284        self.overviewBondsCalendarFile = "overview-calendar.md"
285        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
286
287        See also: `Overview()` with parameter `details="calendar"`.
288        """
289
290        self.reportFile = "deals.md"
291        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
292
293        See also: `Deals()`.
294        """
295
296        self.withdrawalLimitsFile = "limits.md"
297        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
298
299        See also: `OverviewLimits()` and `RequestLimits()`.
300        """
301
302        self.userInfoFile = "user-info.md"
303        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
304
305        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
306        """
307
308        self.userAccountsFile = "accounts.md"
309        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
310
311        See also: `OverviewAccounts()`, `RequestAccounts()`.
312        """
313
314        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
315        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
316
317        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
318
319        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
320        """
321
322        self.iList = None  # init iList for raw instruments data
323        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
324        
325        See also: `Listing()`, `DumpInstruments()`.
326        """
327
328        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
329        if useCache:
330            if os.path.exists(self.iListDumpFile):
331                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
332                curTime = datetime.now(tzutc())
333
334                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
335                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
336
337                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
338
339                else:
340                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
341
342                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
343                        os.path.abspath(self.iListDumpFile),
344                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
345                    ))
346
347            else:
348                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
349                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
350
351        else:
352            self.iList = self.Listing()  # request new raw instruments data from broker server
353            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
354
355        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
356        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
357
358        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
359        """

Main class init.

Parameters
  • token: Bearer token for Tinkoff Invest API. It can be set from environment variable TKS_API_TOKEN.
  • accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. Also, this variable can be set from environment variable TKS_ACCOUNT_ID.
  • useCache: use default cache file with raw data to use instead of iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then set useCache=False.
  • defaultCache: path to default cache file. dump.json by default.
version

Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.

Latest version: https://pypi.org/project/tksbrokerapi/

aliases

Some aliases instead official tickers.

See also: TKSEnums.TKS_TICKER_ALIASES

depth

Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.

See also: GetCurrentPrices().

server

Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest

See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().

timeout

Server operations timeout in seconds. Default: 15.

See also: SendAPIRequest().

headers

Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.

See also: SendAPIRequest().

body

Request body which send to broker server. Default: None.

See also: SendAPIRequest().

moreDebug

Enables more debug information in this class, such as net request and response headers in all methods. False by default.

useHTMLReports

If True then TKSBrokerAPI generate also HTML reports from Markdown. False by default.

See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.

historyFile

Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.

See also: History().

htmlHistoryFile

Full path to the html file where rendered candles chart stored. Default: index.html.

See also: ShowHistoryChart().

instrumentsFile

Filename where full available to user instruments list will be saved. Default: instruments.md.

See also: ShowInstrumentsInfo().

searchResultsFile

Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.

See also: SearchInstruments().

pricesFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: GetListOfPrices().

infoFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().

bondsXLSXFile

Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.

See also: ExtendBondsData().

calendarFile

Filename where bonds payment calendar will be saved. Default: calendar.md.

Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.

See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().

overviewFile

Filename where current portfolio, open trades and orders will be saved. Default: overview.md.

See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().

overviewDigestFile

Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.

See also: Overview() with parameter details="digest".

overviewPositionsFile

Filename where only open positions, without everything else will be saved. Default: overview-positions.md.

See also: Overview() with parameter details="positions".

overviewOrdersFile

Filename where open limits and stop orders will be saved. Default: overview-orders.md.

See also: Overview() with parameter details="orders".

overviewAnalyticsFile

Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.

See also: Overview() with parameter details="analytics".

overviewBondsCalendarFile

Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.

See also: Overview() with parameter details="calendar".

reportFile

Filename where history of deals and trade statistics will be saved. Default: deals.md.

See also: Deals().

withdrawalLimitsFile

Filename where table of funds available for withdrawal will be saved. Default: limits.md.

See also: OverviewLimits() and RequestLimits().

userInfoFile

Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.

See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().

userAccountsFile

Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.

See also: OverviewAccounts(), RequestAccounts().

iListDumpFile

Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.

Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.

See also: DumpInstruments() and DumpInstrumentsAsXLSX().

iList

Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.

See also: Listing(), DumpInstruments().

priceModel

PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.

See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator

ticker: str

Setter for string with ticker, e.g. GOOGL. Tickers may be upper case only.

Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.

See also: SearchByTicker(), SearchInstruments().

figi: str

Setter for string with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6. FIGIs may be upper case only.

See also: SearchByFIGI(), SearchInstruments().

def SendAPIRequest( self, url: str, reqType: str = 'GET', retry: int = 3, pause: int = 5) -> dict:
413    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
414        """
415        Send GET or POST request to broker server and receive JSON object.
416
417        self.header: must be defining with dictionary of headers.
418        self.body: if define then used as request body. None by default.
419        self.timeout: global request timeout, 15 seconds by default.
420        :param url: url with REST request.
421        :param reqType: send "GET" or "POST" request. "GET" by default.
422        :param retry: how many times retry after first request if an 5xx server errors occurred.
423        :param pause: sleep time in seconds between retries.
424        :return: response JSON (dictionary) from broker.
425        """
426        if reqType.upper() not in ("GET", "POST"):
427            uLogger.error("You can define request type: `GET` or `POST`!")
428            raise Exception("Incorrect value")
429
430        if self.moreDebug:
431            uLogger.debug("Request parameters:")
432            uLogger.debug("    - REST API URL: {}".format(url))
433            uLogger.debug("    - request type: {}".format(reqType))
434            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
435            uLogger.debug("    - body:\n{}".format(self.body))
436
437        # fast hack to avoid all operations with some tickers/FIGI
438        responseJSON = {}
439        oK = True
440        for item in self.exclude:
441            if item in url:
442                if self.moreDebug:
443                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
444
445                oK = False
446                break
447
448        if oK:
449            with self.__lock:  # acquire the mutex lock
450                counter = 0
451                response = None
452                errMsg = ""
453
454                while not response and counter <= retry:
455                    if reqType == "GET":
456                        response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
457
458                    if reqType == "POST":
459                        response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
460
461                    if self.moreDebug:
462                        uLogger.debug("Response:")
463                        uLogger.debug("    - status code: {}".format(response.status_code))
464                        uLogger.debug("    - reason: {}".format(response.reason))
465                        uLogger.debug("    - body length: {}".format(len(response.text)))
466                        uLogger.debug("    - headers:\n{}".format(response.headers))
467
468                    # Server returns some headers:
469                    # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
470                    # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
471                    # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
472                    # See: https://tinkoff.github.io/investAPI/grpc/#kreya
473                    if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
474                        rateLimitWait = int(response.headers["x-ratelimit-reset"])
475                        uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
476                        sleep(rateLimitWait)
477
478                    # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
479                    if 400 <= response.status_code < 500:
480                        msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
481                        uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
482
483                        if "code" in response.text and "message" in response.text:
484                            msgDict = self._ParseJSON(rawData=response.text)
485                            uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
486
487                        counter = retry + 1  # do not retry for 4xx errors
488
489                    if 500 <= response.status_code < 600:
490                        errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
491                        uLogger.debug("    - not oK, {}".format(errMsg))
492
493                        if "code" in response.text and "message" in response.text:
494                            errMsgDict = self._ParseJSON(rawData=response.text)
495                            uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
496
497                        counter += 1
498
499                        if counter <= retry:
500                            uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
501                            sleep(pause)
502
503                responseJSON = self._ParseJSON(rawData=response.text)
504
505                if errMsg:
506                    uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
507                    uLogger.error("    - not oK, {}".format(errMsg))
508
509        return responseJSON

Send GET or POST request to broker server and receive JSON object.

self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.

Parameters
  • url: url with REST request.
  • reqType: send "GET" or "POST" request. "GET" by default.
  • retry: how many times retry after first request if an 5xx server errors occurred.
  • pause: sleep time in seconds between retries.
Returns

response JSON (dictionary) from broker.

def Listing(self) -> dict:
542    def Listing(self) -> dict:
543        """
544        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
545
546        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
547        """
548        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
549        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
550
551        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
552        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
553        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
554
555        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
556        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
557        poolUpdater.close()  # close the thread pool
558        poolUpdater.join()  # wait a moment until all data returns from threads
559
560        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
561        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
562        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
563
564        # calculate minimum price increment (step) for all instruments and set up instrument's type:
565        for iType in iList.keys():
566            for ticker in iList[iType]:
567                iList[iType][ticker]["type"] = iType
568
569                if "minPriceIncrement" in iList[iType][ticker].keys():
570                    iList[iType][ticker]["step"] = NanoToFloat(
571                        iList[iType][ticker]["minPriceIncrement"]["units"],
572                        iList[iType][ticker]["minPriceIncrement"]["nano"],
573                    )
574
575                else:
576                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
577
578        return iList

Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.

Returns

Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.

def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
580    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
581        """
582        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
583
584        See also: `DumpInstruments()`, `Listing()`.
585
586        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
587                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
588        """
589        if self.iListDumpFile is None or not self.iListDumpFile:
590            uLogger.error("Output name of dump file must be defined!")
591            raise Exception("Filename required")
592
593        if not self.iList or forceUpdate:
594            self.iList = self.Listing()
595
596        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
597
598        # Save as XLSX with separated sheets for every type of instruments:
599        with pd.ExcelWriter(
600                path=xlsxDumpFile,
601                date_format=TKS_DATE_FORMAT,
602                datetime_format=TKS_DATE_TIME_FORMAT,
603                mode="w",
604        ) as writer:
605            for iType in TKS_INSTRUMENTS:
606                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
607                df = df[sorted(df)]  # sorted by column names
608                df = df.applymap(
609                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
610                    na_action="ignore",
611                )  # converting numbers from nano-type to float in every cell
612                df.to_excel(
613                    writer,
614                    sheet_name=iType,
615                    encoding="UTF-8",
616                    freeze_panes=(1, 1),
617                )  # saving as XLSX-file with freeze first row and column as headers
618
619        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))

Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.

See also: DumpInstruments(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as XLSX-file (default: dump.xlsx) .
def DumpInstruments(self, forceUpdate: bool = True) -> str:
621    def DumpInstruments(self, forceUpdate: bool = True) -> str:
622        """
623        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
624        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
625
626        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
627
628        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
629                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
630        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
631        """
632        if self.iListDumpFile is None or not self.iListDumpFile:
633            uLogger.error("Output name of dump file must be defined!")
634            raise Exception("Filename required")
635
636        if not self.iList or forceUpdate:
637            self.iList = self.Listing()
638
639        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
640        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
641            fH.write(jsonDump)
642
643        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
644
645        return jsonDump

Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server using Listing() method. If iListDumpFile string is not empty then also save information to this file.

See also: DumpInstrumentsAsXLSX(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as JSON-file (default: dump.json).
Returns

serialized JSON formatted str with full data of instruments, also saved to the --output JSON-file.

def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
647    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
648        """
649        Show information about one instrument defined by json data and prints it in Markdown format.
650
651        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
652
653        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
654        :param show: if `True` then also printing information about instrument and its current price.
655        :return: multilines text in Markdown format with information about one instrument.
656        """
657        splitLine = "|                                                             |                                                        |\n"
658        infoText = ""
659
660        if iJSON is not None and iJSON and isinstance(iJSON, dict):
661            info = [
662                "# Main information\n\n",
663                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
664                "| Parameters                                                  | Values                                                 |\n",
665                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
666                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
667                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
668            ]
669
670            if "sector" in iJSON.keys() and iJSON["sector"]:
671                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
672
673            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
674                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
675
676            info.extend([
677                splitLine,
678                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
679                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
680            ])
681
682            if "isin" in iJSON.keys() and iJSON["isin"]:
683                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
684
685            if "classCode" in iJSON.keys():
686                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
687
688            info.extend([
689                splitLine,
690                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
691                splitLine,
692                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
693                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
694                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
695            ])
696
697            if iJSON["figi"]:
698                self._figi = iJSON["figi"]
699                iJSON = iJSON | self.RequestTradingStatus()
700
701                info.extend([
702                    splitLine,
703                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
704                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
705                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
706                ])
707
708            info.append(splitLine)
709
710            if "type" in iJSON.keys() and iJSON["type"]:
711                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
712
713                if "shareType" in iJSON.keys() and iJSON["shareType"]:
714                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
715
716            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
717                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
718
719            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
720                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
721
722            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
723                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
724
725            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
726                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
727
728            if "focusType" in iJSON.keys() and iJSON["focusType"]:
729                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
730
731            if "assetType" in iJSON.keys() and iJSON["assetType"]:
732                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
733
734            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
735                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
736
737            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
738                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
739
740            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
741                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
742
743            if "currency" in iJSON.keys():
744                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
745
746            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
747                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
748
749            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
750                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
751
752            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
753                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
754
755            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
756                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
757
758            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
759                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
760
761            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
762                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
763
764            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
765                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
766
767            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
768                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
769
770            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
771                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
772
773            iExt = None
774            if iJSON["type"] == "Bonds":
775                info.extend([
776                    splitLine,
777                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
778                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
779                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
780                        iJSON["nominal"]["currency"],
781                    )),
782                ])
783
784                if "floatingCouponFlag" in iJSON.keys():
785                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
786
787                if "amortizationFlag" in iJSON.keys():
788                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
789
790                info.append(splitLine)
791
792                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
793                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
794
795                if iJSON["figi"]:
796                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
797
798                    info.extend([
799                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
800                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
801                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
802                    ])
803
804                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
805                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
806                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
807                        iJSON["aciValue"]["currency"]
808                    )))
809
810            if "currentPrice" in iJSON.keys():
811                info.append(splitLine)
812
813                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
814                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
815
816                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
817                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
818                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
819                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
820                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
821
822                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
823                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
824
825                info.extend([
826                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
827                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
828                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
829                    )),
830                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
831                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
832                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
833                    )),
834                    "| Changes between last deal price and last close              | {:<54} |\n".format(
835                        "{:.2f}%{}".format(
836                            iJSON["currentPrice"]["changes"],
837                            " ({}{:.2f} {})".format(
838                                "+" if bondChangesDelta > 0 else "",
839                                bondChangesDelta,
840                                aciCurrency
841                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
842                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
843                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
844                                currency
845                            ),
846                        )
847                    ),
848                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
849                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
850                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
851                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
852                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
853                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
854                    )),
855                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
856                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
857                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
858                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
859                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
860                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
861                    )),
862                ])
863
864            if "lot" in iJSON.keys():
865                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
866
867            if "step" in iJSON.keys() and iJSON["step"] != 0:
868                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
869
870            # Add bond payment calendar:
871            if iJSON["type"] == "Bonds":
872                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
873                info.extend(["\n#", strCalendar])
874
875            infoText += "".join(info)
876
877            if show:
878                uLogger.info("{}".format(infoText))
879
880            else:
881                uLogger.debug("{}".format(infoText))
882
883            if self.infoFile is not None:
884                with open(self.infoFile, "w", encoding="UTF-8") as fH:
885                    fH.write(infoText)
886
887                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
888
889                if self.useHTMLReports:
890                    htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html"
891                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
892                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText))
893
894                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
895
896        return infoText

Show information about one instrument defined by json data and prints it in Markdown format.

See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().

Parameters
  • iJSON: json data of instrument, example: iJSON = self.iList["Shares"][self._ticker]
  • show: if True then also printing information about instrument and its current price.
Returns

multilines text in Markdown format with information about one instrument.

def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
898    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
899        """
900        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
901
902        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
903        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
904        :return: JSON formatted data with information about instrument.
905        """
906        tickerJSON = {}
907        if self.moreDebug:
908            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
909
910        if not self._ticker:
911            uLogger.warning("self._ticker variable is not be empty!")
912
913        else:
914            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
915                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
916                raise Exception("Instrument not allowed")
917
918            if not self.iList:
919                self.iList = self.Listing()
920
921            if self._ticker in self.iList["Shares"].keys():
922                tickerJSON = self.iList["Shares"][self._ticker]
923                if self.moreDebug:
924                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
925
926            elif self._ticker in self.iList["Currencies"].keys():
927                tickerJSON = self.iList["Currencies"][self._ticker]
928                if self.moreDebug:
929                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
930
931            elif self._ticker in self.iList["Bonds"].keys():
932                tickerJSON = self.iList["Bonds"][self._ticker]
933                if self.moreDebug:
934                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
935
936            elif self._ticker in self.iList["Etfs"].keys():
937                tickerJSON = self.iList["Etfs"][self._ticker]
938                if self.moreDebug:
939                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
940
941            elif self._ticker in self.iList["Futures"].keys():
942                tickerJSON = self.iList["Futures"][self._ticker]
943                if self.moreDebug:
944                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
945
946        if tickerJSON:
947            self._figi = tickerJSON["figi"]
948
949            if requestPrice:
950                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
951
952                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
953                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
954
955                else:
956                    tickerJSON["currentPrice"]["changes"] = 0
957
958            if show:
959                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
960
961        else:
962            if show:
963                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
964
965        return tickerJSON

Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (because this is long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 967    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 968        """
 969        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
 970
 971        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
 972        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 973        :return: JSON formatted data with information about instrument.
 974        """
 975        figiJSON = {}
 976        if self.moreDebug:
 977            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
 978
 979        if not self._figi:
 980            uLogger.warning("self._figi variable is not be empty!")
 981
 982        else:
 983            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
 984                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
 985                raise Exception("Instrument not allowed")
 986
 987            if not self.iList:
 988                self.iList = self.Listing()
 989
 990            for item in self.iList["Shares"].keys():
 991                if self._figi == self.iList["Shares"][item]["figi"]:
 992                    figiJSON = self.iList["Shares"][item]
 993
 994                    if self.moreDebug:
 995                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
 996
 997                    break
 998
 999            if not figiJSON:
1000                for item in self.iList["Currencies"].keys():
1001                    if self._figi == self.iList["Currencies"][item]["figi"]:
1002                        figiJSON = self.iList["Currencies"][item]
1003
1004                        if self.moreDebug:
1005                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
1006
1007                        break
1008
1009            if not figiJSON:
1010                for item in self.iList["Bonds"].keys():
1011                    if self._figi == self.iList["Bonds"][item]["figi"]:
1012                        figiJSON = self.iList["Bonds"][item]
1013
1014                        if self.moreDebug:
1015                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
1016
1017                        break
1018
1019            if not figiJSON:
1020                for item in self.iList["Etfs"].keys():
1021                    if self._figi == self.iList["Etfs"][item]["figi"]:
1022                        figiJSON = self.iList["Etfs"][item]
1023
1024                        if self.moreDebug:
1025                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1026
1027                        break
1028
1029            if not figiJSON:
1030                for item in self.iList["Futures"].keys():
1031                    if self._figi == self.iList["Futures"][item]["figi"]:
1032                        figiJSON = self.iList["Futures"][item]
1033
1034                        if self.moreDebug:
1035                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1036
1037                        break
1038
1039        if figiJSON:
1040            self._figi = figiJSON["figi"]
1041            self._ticker = figiJSON["ticker"]
1042
1043            if requestPrice:
1044                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1045
1046                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1047                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1048
1049                else:
1050                    figiJSON["currentPrice"]["changes"] = 0
1051
1052            if show:
1053                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1054
1055        else:
1056            if show:
1057                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1058
1059        return figiJSON

Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (it's long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def GetCurrentPrices(self, show: bool = True) -> dict:
1061    def GetCurrentPrices(self, show: bool = True) -> dict:
1062        """
1063        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1064        `{"buy": [{"price": 1243.8, "quantity": 193},
1065                  {"price": 1244.0, "quantity": 168},
1066                  {"price": 1244.8, "quantity": 5},
1067                  {"price": 1245.0, "quantity": 61},
1068                  {"price": 1245.4, "quantity": 60}],
1069          "sell": [{"price": 1243.6, "quantity": 8},
1070                   {"price": 1242.6, "quantity": 10},
1071                   {"price": 1242.4, "quantity": 18},
1072                   {"price": 1242.2, "quantity": 50},
1073                   {"price": 1242.0, "quantity": 113}],
1074          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1075        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1076        - sell: list of dicts with Buyers prices,
1077            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1078            - quantity: volume value by current price in lots,
1079        - limitUp: current trade session limit price, maximum,
1080        - limitDown: current trade session limit price, minimum,
1081        - lastPrice: last deal price of the instrument,
1082        - closePrice: previous trade session close price of the instrument.
1083
1084        See also: `SearchByTicker()` and `SearchByFIGI()`.
1085        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1086        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1087
1088        :param show: if `True` then print DOM to log and console.
1089        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1090                 If an error occurred then returns an empty record:
1091                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1092        """
1093        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1094
1095        if self.depth < 1:
1096            uLogger.error("Depth of Market (DOM) must be >=1!")
1097            raise Exception("Incorrect value")
1098
1099        if not (self._ticker or self._figi):
1100            uLogger.error("self._ticker or self._figi variables must be defined!")
1101            raise Exception("Ticker or FIGI required")
1102
1103        if self._ticker and not self._figi:
1104            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1105            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1106
1107        if not self._ticker and self._figi:
1108            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1109            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1110
1111        if not self._figi:
1112            uLogger.error("FIGI is not defined!")
1113            raise Exception("Ticker or FIGI required")
1114
1115        else:
1116            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1117
1118            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1119            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1120            self.body = str({"figi": self._figi, "depth": self.depth})
1121            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1122
1123            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1124                # list of dicts with sellers orders:
1125                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1126
1127                # list of dicts with buyers orders:
1128                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1129
1130                # max price of instrument at this time:
1131                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1132
1133                # min price of instrument at this time:
1134                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1135
1136                # last price of deal with instrument:
1137                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1138
1139                # last close price of instrument:
1140                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1141
1142            else:
1143                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1144                uLogger.debug("Server response: {}".format(pricesResponse))
1145
1146            if show:
1147                if prices["buy"] or prices["sell"]:
1148                    info = [
1149                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1150                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1151                            self._ticker,
1152                            self._figi,
1153                            self.depth,
1154                        ),
1155                        "-" * 60, "\n",
1156                        "             Orders of Buyers | Orders of Sellers\n",
1157                        "-" * 60, "\n",
1158                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1159                        "-" * 60, "\n",
1160                    ]
1161
1162                    if not prices["buy"]:
1163                        info.append("                              | No orders!\n")
1164                        sumBuy = 0
1165
1166                    else:
1167                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1168                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1169                        for item in maxMinSorted:
1170                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1171
1172                    if not prices["sell"]:
1173                        info.append("No orders!                    |\n")
1174                        sumSell = 0
1175
1176                    else:
1177                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1178                        for item in prices["sell"]:
1179                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1180
1181                    info.extend([
1182                        "-" * 60, "\n",
1183                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1184                        "-" * 60, "\n",
1185                    ])
1186
1187                    infoText = "".join(info)
1188
1189                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1190
1191                else:
1192                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1193
1194        return prices

Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5: {"buy": [{"price": 1243.8, "quantity": 193}, {"price": 1244.0, "quantity": 168}, {"price": 1244.8, "quantity": 5}, {"price": 1245.0, "quantity": 61}, {"price": 1245.4, "quantity": 60}], "sell": [{"price": 1243.6, "quantity": 8}, {"price": 1242.6, "quantity": 10}, {"price": 1242.4, "quantity": 18}, {"price": 1242.2, "quantity": 50}, {"price": 1242.0, "quantity": 113}], "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:

  • buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
  • sell: list of dicts with Buyers prices,
    • price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
    • quantity: volume value by current price in lots,
  • limitUp: current trade session limit price, maximum,
  • limitDown: current trade session limit price, minimum,
  • lastPrice: last deal price of the instrument,
  • closePrice: previous trade session close price of the instrument.

See also: SearchByTicker() and SearchByFIGI(). REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse

Parameters
  • show: if True then print DOM to log and console.
Returns

orders book dict with lists of current buy and sell prices: {"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record: {"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.

def ShowInstrumentsInfo(self, show: bool = True) -> str:
1196    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1197        """
1198        This method get and show information about all available broker instruments for current user account.
1199        If `instrumentsFile` string is not empty then also save information to this file.
1200
1201        :param show: if `True` then print results to console, if `False` — print only to file.
1202        :return: multi-lines string with all available broker instruments
1203        """
1204        if not self.iList:
1205            self.iList = self.Listing()
1206
1207        info = [
1208            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1209            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1210        ]
1211
1212        # add instruments count by type:
1213        for iType in self.iList.keys():
1214            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1215
1216        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1217        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1218
1219        # generating info tables with all instruments by type:
1220        for iType in self.iList.keys():
1221            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1222
1223            for instrument in self.iList[iType].keys():
1224                iName = self.iList[iType][instrument]["name"]  # instrument's name
1225                if len(iName) > 57:
1226                    iName = "{}...".format(iName[:54])  # right trim for a long string
1227
1228                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1229                    self.iList[iType][instrument]["ticker"],
1230                    iName,
1231                    self.iList[iType][instrument]["figi"],
1232                    self.iList[iType][instrument]["currency"],
1233                    self.iList[iType][instrument]["lot"],
1234                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1235                ))
1236
1237        infoText = "".join(info)
1238
1239        if show:
1240            uLogger.info(infoText)
1241
1242        if self.instrumentsFile:
1243            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1244                fH.write(infoText)
1245
1246            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1247
1248            if self.useHTMLReports:
1249                htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html"
1250                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1251                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText))
1252
1253                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1254
1255        return infoText

This method get and show information about all available broker instruments for current user account. If instrumentsFile string is not empty then also save information to this file.

Parameters
  • show: if True then print results to console, if False — print only to file.
Returns

multi-lines string with all available broker instruments

def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1257    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1258        """
1259        This method search and show information about instruments by part of its ticker, FIGI or name.
1260        If `searchResultsFile` string is not empty then also save information to this file.
1261
1262        :param pattern: string with part of ticker, FIGI or instrument's name.
1263        :param show: if `True` then print results to console, if `False` — return list of result only.
1264        :return: list of dictionaries with all found instruments.
1265        """
1266        if not self.iList:
1267            self.iList = self.Listing()
1268
1269        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contain only filtered instruments
1270        compiledPattern = re.compile(pattern, re.IGNORECASE)
1271
1272        for iType in self.iList:
1273            for instrument in self.iList[iType].values():
1274                searchResult = compiledPattern.search(" ".join(
1275                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1276                ))
1277
1278                if searchResult:
1279                    searchResults[iType][instrument["ticker"]] = instrument
1280
1281        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1282        info = [
1283            "# Search results\n\n",
1284            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1285            "* **Search pattern:** [{}]\n".format(pattern),
1286            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1287            '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n'
1288        ]
1289        infoShort = info[:]
1290
1291        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1292        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1293        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1294
1295        if resultsLen == 0:
1296            info.append("\nNo results\n")
1297            infoShort.append("\nNo results\n")
1298            uLogger.warning("No results. Try changing your search pattern.")
1299
1300        else:
1301            for iType in searchResults:
1302                iTypeValuesCount = len(searchResults[iType].values())
1303                if iTypeValuesCount > 0:
1304                    info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1305                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1306
1307                    for instrument in searchResults[iType].values():
1308                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1309                            instrument["type"],
1310                            instrument["ticker"],
1311                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1312                            instrument["figi"],
1313                        ))
1314
1315                    if iTypeValuesCount <= 5:
1316                        infoShort.extend(info[-iTypeValuesCount:])
1317
1318                    else:
1319                        infoShort.extend(info[-5:])
1320                        infoShort.append(skippedLine)
1321
1322        infoText = "".join(info)
1323        infoTextShort = "".join(infoShort)
1324
1325        if show:
1326            uLogger.info(infoTextShort)
1327            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1328
1329        if self.searchResultsFile:
1330            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1331                fH.write(infoText)
1332
1333            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1334
1335            if self.useHTMLReports:
1336                htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html"
1337                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1338                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText))
1339
1340                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1341
1342        return searchResults

This method search and show information about instruments by part of its ticker, FIGI or name. If searchResultsFile string is not empty then also save information to this file.

Parameters
  • pattern: string with part of ticker, FIGI or instrument's name.
  • show: if True then print results to console, if False — return list of result only.
Returns

list of dictionaries with all found instruments.

def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1344    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1345        """
1346        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1347
1348        :param instruments: list of strings with tickers or FIGIs.
1349        :return: list with unique instrument FIGIs only.
1350        """
1351        requestedInstruments = []
1352        for iName in instruments:
1353            if iName not in self.aliases.keys():
1354                if iName not in requestedInstruments:
1355                    requestedInstruments.append(iName)
1356
1357            else:
1358                if iName not in requestedInstruments:
1359                    if self.aliases[iName] not in requestedInstruments:
1360                        requestedInstruments.append(self.aliases[iName])
1361
1362        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1363
1364        onlyUniqueFIGIs = []
1365        for iName in requestedInstruments:
1366            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1367                continue
1368
1369            self._ticker = iName
1370            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1371
1372            if not iData:
1373                self._ticker = ""
1374                self._figi = iName
1375
1376                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1377
1378                if not iData:
1379                    self._figi = ""
1380                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1381
1382            if iData and iData["figi"] not in onlyUniqueFIGIs:
1383                onlyUniqueFIGIs.append(iData["figi"])
1384
1385        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1386
1387        return onlyUniqueFIGIs

Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.

Parameters
  • instruments: list of strings with tickers or FIGIs.
Returns

list with unique instrument FIGIs only.

def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]:
1389    def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]:
1390        """
1391        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1392
1393        See limits: https://tinkoff.github.io/investAPI/limits/
1394
1395        If `pricesFile` string is not empty then also save information to this file.
1396
1397        :param instruments: list of strings with tickers or FIGIs.
1398        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1399        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1400                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1401        """
1402        if instruments is None or not instruments:
1403            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1404            raise Exception("Ticker or FIGI required")
1405
1406        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1407
1408        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1409
1410        iList = []  # trying to get info and current prices about all unique instruments:
1411        for self._figi in onlyUniqueFIGIs:
1412            iData = self.SearchByFIGI(requestPrice=True)
1413            iList.append(iData)
1414
1415        self.ShowListOfPrices(iList, show)
1416
1417        return iList

This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!

See limits: https://tinkoff.github.io/investAPI/limits/

If pricesFile string is not empty then also save information to this file.

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
Returns

list of instruments looks like [{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker() or SearchByFIGI() methods.

def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1419    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1420        """
1421        Show table contains current prices of given instruments.
1422
1423        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1424                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1425        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1426        :return: multilines text in Markdown format as a table contains current prices.
1427        """
1428        infoText = ""
1429
1430        if show or self.pricesFile:
1431            info = [
1432                "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1433                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1434                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1435            ]
1436
1437            for item in iList:
1438                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1439                    item["ticker"],
1440                    item["figi"],
1441                    item["type"],
1442                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1443                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1444                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1445                    "{} / {}".format(
1446                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1447                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1448                    ),
1449                    "{} / {}".format(
1450                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1451                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1452                    ),
1453                    item["currency"],
1454                ))
1455
1456            infoText = "".join(info)
1457
1458            if show:
1459                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1460
1461            if self.pricesFile:
1462                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1463                    fH.write(infoText)
1464
1465                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1466
1467                if self.useHTMLReports:
1468                    htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html"
1469                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1470                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText))
1471
1472                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1473
1474        return infoText

Show table contains current prices of given instruments.

Parameters
  • **iList: list of instruments looks like [{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker(requestPrice=True) or by SearchByFIGI(requestPrice=True) methods.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
Returns

multilines text in Markdown format as a table contains current prices.

def RequestTradingStatus(self) -> dict:
1476    def RequestTradingStatus(self) -> dict:
1477        """
1478        Requesting trading status for the instrument defined by `figi` variable.
1479
1480        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1481
1482        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1483
1484        :return: dictionary with trading status attributes. Response example:
1485                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1486                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1487        """
1488        if self._figi is None or not self._figi:
1489            uLogger.error("Variable `figi` must be defined for using this method!")
1490            raise Exception("FIGI required")
1491
1492        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1493
1494        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1495        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1496        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1497
1498        if self.moreDebug:
1499            uLogger.debug("Records about current trading status successfully received")
1500
1501        return tradingStatus

Requesting trading status for the instrument defined by figi variable.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus

Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest

Returns

dictionary with trading status attributes. Response example: {"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}

def RequestPortfolio(self) -> dict:
1503    def RequestPortfolio(self) -> dict:
1504        """
1505        Requesting actual user's portfolio for current `accountId`.
1506
1507        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1508
1509        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1510
1511        :return: dictionary with user's portfolio.
1512        """
1513        if self.accountId is None or not self.accountId:
1514            uLogger.error("Variable `accountId` must be defined for using this method!")
1515            raise Exception("Account ID required")
1516
1517        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1518
1519        self.body = str({"accountId": self.accountId})
1520        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1521        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1522
1523        if self.moreDebug:
1524            uLogger.debug("Records about user's portfolio successfully received")
1525
1526        return rawPortfolio

Requesting actual user's portfolio for current accountId.

REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio

Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest

Returns

dictionary with user's portfolio.

def RequestPositions(self) -> dict:
1528    def RequestPositions(self) -> dict:
1529        """
1530        Requesting open positions by currencies and instruments for current `accountId`.
1531
1532        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1533
1534        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1535
1536        :return: dictionary with open positions by instruments.
1537        """
1538        if self.accountId is None or not self.accountId:
1539            uLogger.error("Variable `accountId` must be defined for using this method!")
1540            raise Exception("Account ID required")
1541
1542        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1543
1544        self.body = str({"accountId": self.accountId})
1545        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1546        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1547
1548        if self.moreDebug:
1549            uLogger.debug("Records about current open positions successfully received")
1550
1551        return rawPositions

Requesting open positions by currencies and instruments for current accountId.

REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions

Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest

Returns

dictionary with open positions by instruments.

def RequestPendingOrders(self) -> list:
1553    def RequestPendingOrders(self) -> list:
1554        """
1555        Requesting current actual pending limit orders for current `accountId`.
1556
1557        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1558
1559        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1560
1561        :return: list of dictionaries with pending limit orders.
1562        """
1563        if self.accountId is None or not self.accountId:
1564            uLogger.error("Variable `accountId` must be defined for using this method!")
1565            raise Exception("Account ID required")
1566
1567        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1568
1569        self.body = str({"accountId": self.accountId})
1570        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1571        rawResponse = self.SendAPIRequest(ordersURL, reqType="POST")
1572
1573        if "orders" in rawResponse.keys():
1574            rawOrders = rawResponse["orders"]
1575            uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1576
1577        else:
1578            rawOrders = []
1579            uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse))
1580
1581        return rawOrders

Requesting current actual pending limit orders for current accountId.

REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders

Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest

Returns

list of dictionaries with pending limit orders.

def RequestStopOrders(self) -> list:
1583    def RequestStopOrders(self) -> list:
1584        """
1585        Requesting current actual stop orders for current `accountId`.
1586
1587        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1588
1589        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1590
1591        :return: list of dictionaries with stop orders.
1592        """
1593        if self.accountId is None or not self.accountId:
1594            uLogger.error("Variable `accountId` must be defined for using this method!")
1595            raise Exception("Account ID required")
1596
1597        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1598
1599        self.body = str({"accountId": self.accountId})
1600        stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1601        rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST")
1602
1603        if "stopOrders" in rawResponse.keys():
1604            rawStopOrders = rawResponse["stopOrders"]
1605            uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1606
1607        else:
1608            rawStopOrders = []
1609            uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse))
1610
1611        return rawStopOrders

Requesting current actual stop orders for current accountId.

REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders

Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest

Returns

list of dictionaries with stop orders.

def Overview(self, show: bool = False, details: str = 'full') -> dict:
1613    def Overview(self, show: bool = False, details: str = "full") -> dict:
1614        """
1615        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1616        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1617        and `overviewBondsCalendarFile` are defined then also save information to file.
1618
1619        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1620        many requests about the state of the portfolio, and then, based on the received data, a large number
1621        of calculation and statistics are collected.
1622
1623        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1624        :param details: how detailed should the information be?
1625        - `full` — shows full available information about portfolio status (by default),
1626        - `positions` — shows only open positions,
1627        - `orders` — shows only sections of open limits and stop orders.
1628        - `digest` — show a short digest of the portfolio status,
1629        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1630        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1631        :return: dictionary with client's raw portfolio and some statistics.
1632        """
1633        if self.accountId is None or not self.accountId:
1634            uLogger.error("Variable `accountId` must be defined for using this method!")
1635            raise Exception("Account ID required")
1636
1637        view = {
1638            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1639                "headers": {},  # list of dictionaries, response headers without "positions" section
1640                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1641                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1642                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1643                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1644                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1645                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1646                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1647                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1648                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1649            },
1650            "stat": {  # --- some statistics calculated using "raw" sections:
1651                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1652                "availableRUB": 0.,  # available rubles (without other currencies)
1653                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1654                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1655                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1656                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1657                "sharesCostRUB": 0.,  # costs of all shares in RUB
1658                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1659                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1660                "futuresCostRUB": 0.,  # costs of all futures in RUB
1661                "Currencies": [],  # list of dictionaries of all currencies statistics
1662                "Shares": [],  # list of dictionaries of all shares statistics
1663                "Bonds": [],  # list of dictionaries of all bonds statistics
1664                "Etfs": [],  # list of dictionaries of all etfs statistics
1665                "Futures": [],  # list of dictionaries of all futures statistics
1666                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1667                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1668                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1669                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1670                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1671            },
1672            "analytics": {  # --- some analytics of portfolio:
1673                "distrByAssets": {},  # portfolio distribution by assets
1674                "distrByCompanies": {},  # portfolio distribution by companies
1675                "distrBySectors": {},  # portfolio distribution by sectors
1676                "distrByCurrencies": {},  # portfolio distribution by currencies
1677                "distrByCountries": {},  # portfolio distribution by countries
1678                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1679            }
1680        }
1681
1682        details = details.lower()
1683        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1684        if details not in availableDetails:
1685            details = "full"
1686            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1687
1688        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1689
1690        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1691        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1692        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1693        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1694
1695        # save response headers without "positions" section:
1696        for key in portfolioResponse.keys():
1697            if key != "positions":
1698                view["raw"]["headers"][key] = portfolioResponse[key]
1699
1700            else:
1701                continue
1702
1703        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1704        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1705        for item in portfolioResponse["positions"]:
1706            if item["instrumentType"] == "currency":
1707                self._figi = item["figi"]
1708                if not self._figi and item["ticker"]:
1709                    self._ticker = item["ticker"]
1710                    self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1711
1712                curr = self.SearchByFIGI(requestPrice=False)
1713
1714                # current price of currency in RUB:
1715                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1716                    "name": curr["name"],
1717                    "currentPrice": NanoToFloat(
1718                        item["currentPrice"]["units"],
1719                        item["currentPrice"]["nano"]
1720                    ),
1721                }
1722
1723                view["raw"]["Currencies"].append(item)
1724
1725            elif item["instrumentType"] == "share":
1726                view["raw"]["Shares"].append(item)
1727
1728            elif item["instrumentType"] == "bond":
1729                view["raw"]["Bonds"].append(item)
1730
1731            elif item["instrumentType"] == "etf":
1732                view["raw"]["Etfs"].append(item)
1733
1734            elif item["instrumentType"] == "futures":
1735                view["raw"]["Futures"].append(item)
1736
1737            else:
1738                continue
1739
1740        # how many volume of currencies (by ISO currency name) are blocked:
1741        for item in view["raw"]["positions"]["blocked"]:
1742            blocked = NanoToFloat(item["units"], item["nano"])
1743            if blocked > 0:
1744                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1745
1746        # how many volume of instruments (by FIGI) are blocked:
1747        for item in view["raw"]["positions"]["securities"]:
1748            blocked = int(item["blocked"])
1749            if blocked > 0:
1750                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1751
1752        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1753
1754        if "rub" in allBlocked.keys():
1755            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1756
1757        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1758        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1759        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1760        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1761        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1762        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1763        view["stat"]["portfolioCostRUB"] = sum([
1764            view["stat"]["allCurrenciesCostRUB"],
1765            view["stat"]["sharesCostRUB"],
1766            view["stat"]["bondsCostRUB"],
1767            view["stat"]["etfsCostRUB"],
1768            view["stat"]["futuresCostRUB"],
1769        ])
1770
1771        # --- calculating some portfolio statistics:
1772        byComp = {}  # distribution by companies
1773        bySect = {}  # distribution by sectors
1774        byCurr = {}  # distribution by currencies (include RUB)
1775        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1776        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1777
1778        for item in portfolioResponse["positions"]:
1779            self._figi = item["figi"]
1780            if not self._figi and item["ticker"]:
1781                self._ticker = item["ticker"]
1782                self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1783
1784            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1785
1786            if instrument:
1787                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1788                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1789
1790                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1791                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1792
1793                else:
1794                    blocked = 0
1795
1796                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1797                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1798                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1799                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1800                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1801                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1802                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1803                cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1804                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1805                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1806                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1807                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1808
1809                statData = {
1810                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1811                    "ticker": instrument["ticker"],  # ticker by FIGI
1812                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1813                    "volume": volume,  # available volume of instrument
1814                    "lots": lots,  # volume in lots of instrument
1815                    "direction": direction,  # direction of an instrument's position: short or long
1816                    "blocked": blocked,  # blocked volume of currency or instrument
1817                    "currentPrice": curPrice,  # current instrument's price in basic asset
1818                    "average": average,  # current average position price
1819                    "cost": cost,  # current cost of all volume of instrument in basic asset
1820                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1821                    "costRUB": costRUB,  # cost of instrument in ruble
1822                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1823                    "profit": profit,  # expected profit at current moment
1824                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1825                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1826                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1827                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1828                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1829                    "step": instrument["step"],  # minimum price increment
1830                }
1831
1832                # adding distribution by unique countries:
1833                if statData["country"] not in byCountry.keys():
1834                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1835
1836                else:
1837                    byCountry[statData["country"]]["cost"] += costRUB
1838                    byCountry[statData["country"]]["percent"] += percentCostRUB
1839
1840                if item["instrumentType"] != "currency":
1841                    # adding distribution by unique companies:
1842                    if statData["name"]:
1843                        if statData["name"] not in byComp.keys():
1844                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1845
1846                        else:
1847                            byComp[statData["name"]]["cost"] += costRUB
1848                            byComp[statData["name"]]["percent"] += percentCostRUB
1849
1850                    # adding distribution by unique sectors:
1851                    if statData["sector"] not in bySect.keys():
1852                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1853
1854                    else:
1855                        bySect[statData["sector"]]["cost"] += costRUB
1856                        bySect[statData["sector"]]["percent"] += percentCostRUB
1857
1858                # adding distribution by unique currencies:
1859                if currency not in byCurr.keys():
1860                    byCurr[currency] = {
1861                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1862                        "cost": costRUB,
1863                        "percent": percentCostRUB
1864                    }
1865
1866                else:
1867                    byCurr[currency]["cost"] += costRUB
1868                    byCurr[currency]["percent"] += percentCostRUB
1869
1870                # saving statistics for every instrument:
1871                if item["instrumentType"] == "currency":
1872                    view["stat"]["Currencies"].append(statData)
1873
1874                    # update dict with free funds for trading (total - blocked) by currencies
1875                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1876                    view["stat"]["funds"][currency] = {
1877                        "total": volume,
1878                        "totalCostRUB": costRUB,  # total volume cost in rubles
1879                        "free": volume - blocked,
1880                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1881                    }
1882
1883                elif item["instrumentType"] == "share":
1884                    view["stat"]["Shares"].append(statData)
1885
1886                elif item["instrumentType"] == "bond":
1887                    view["stat"]["Bonds"].append(statData)
1888
1889                elif item["instrumentType"] == "etf":
1890                    view["stat"]["Etfs"].append(statData)
1891
1892                elif item["instrumentType"] == "Futures":
1893                    view["stat"]["Futures"].append(statData)
1894
1895                else:
1896                    continue
1897
1898        # total changes in Russian Ruble:
1899        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1900        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1901        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1902        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1903        view["stat"]["funds"]["rub"] = {
1904            "total": view["stat"]["availableRUB"],
1905            "totalCostRUB": view["stat"]["availableRUB"],
1906            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1907            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1908        }
1909
1910        # --- pending limit orders sector data:
1911        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1912        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1913
1914        for item in view["raw"]["orders"]:
1915            self._figi = item["figi"]
1916
1917            if item["figi"] not in uniquePendingOrdersFIGIs:
1918                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1919
1920                uniquePendingOrdersFIGIs.append(item["figi"])
1921                uniquePendingOrders[item["figi"]] = instrument
1922
1923            else:
1924                instrument = uniquePendingOrders[item["figi"]]
1925
1926            if instrument:
1927                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1928                orderType = TKS_ORDER_TYPES[item["orderType"]]
1929                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1930                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1931
1932                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1933                if item["direction"] == "ORDER_DIRECTION_BUY":
1934                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1935
1936                else:
1937                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1938
1939                # requested price for order execution:
1940                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1941
1942                # necessary changes in percent to reach target from current price:
1943                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1944
1945                view["stat"]["orders"].append({
1946                    "orderID": item["orderId"],  # orderId number parameter of current order
1947                    "figi": item["figi"],  # FIGI identification
1948                    "ticker": instrument["ticker"],  # ticker name by FIGI
1949                    "lotsRequested": item["lotsRequested"],  # requested lots value
1950                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1951                    "currentPrice": lastPrice,  # current instrument's price for defined action
1952                    "targetPrice": target,  # requested price for order execution in base currency
1953                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1954                    "percentChanges": changes,  # changes in percent to target from current price
1955                    "currency": item["currency"],  # instrument's currency name
1956                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1957                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1958                    "status": orderState,  # order status from TKS_ORDER_STATES
1959                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1960                })
1961
1962        # --- stop orders sector data:
1963        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1964        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1965
1966        for item in view["raw"]["stopOrders"]:
1967            self._figi = item["figi"]
1968
1969            if item["figi"] not in uniqueStopOrdersFIGIs:
1970                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1971
1972                uniqueStopOrdersFIGIs.append(item["figi"])
1973                uniqueStopOrders[item["figi"]] = instrument
1974
1975            else:
1976                instrument = uniqueStopOrders[item["figi"]]
1977
1978            if instrument:
1979                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1980                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1981                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1982
1983                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1984                if "expirationTime" in item.keys():
1985                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1986                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1987
1988                else:
1989                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1990                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1991
1992                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1993                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1994                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1995
1996                else:
1997                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1998
1999                # requested price when stop-order executed:
2000                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
2001
2002                # price for limit-order, set up when stop-order executed:
2003                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
2004
2005                # necessary changes in percent to reach target from current price:
2006                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
2007
2008                view["stat"]["stopOrders"].append({
2009                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2010                    "figi": item["figi"],  # FIGI identification
2011                    "ticker": instrument["ticker"],  # ticker name by FIGI
2012                    "lotsRequested": item["lotsRequested"],  # requested lots value
2013                    "currentPrice": lastPrice,  # current instrument's price for defined action
2014                    "targetPrice": target,  # requested price for stop-order execution in base currency
2015                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2016                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2017                    "percentChanges": changes,  # changes in percent to target from current price
2018                    "currency": item["currency"],  # instrument's currency name
2019                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2020                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2021                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2022                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2023                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2024                })
2025
2026        # --- calculating data for analytics section:
2027        # portfolio distribution by assets:
2028        view["analytics"]["distrByAssets"] = {
2029            "Ruble": {
2030                "uniques": 1,
2031                "cost": view["stat"]["availableRUB"],
2032                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2033            },
2034            "Currencies": {
2035                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2036                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2037                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2038            },
2039            "Shares": {
2040                "uniques": len(view["stat"]["Shares"]),
2041                "cost": view["stat"]["sharesCostRUB"],
2042                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2043            },
2044            "Bonds": {
2045                "uniques": len(view["stat"]["Bonds"]),
2046                "cost": view["stat"]["bondsCostRUB"],
2047                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2048            },
2049            "Etfs": {
2050                "uniques": len(view["stat"]["Etfs"]),
2051                "cost": view["stat"]["etfsCostRUB"],
2052                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2053            },
2054            "Futures": {
2055                "uniques": len(view["stat"]["Futures"]),
2056                "cost": view["stat"]["futuresCostRUB"],
2057                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2058            },
2059        }
2060
2061        # portfolio distribution by companies:
2062        view["analytics"]["distrByCompanies"]["All money cash"] = {
2063            "ticker": "",
2064            "cost": view["stat"]["allCurrenciesCostRUB"],
2065            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2066        }
2067        view["analytics"]["distrByCompanies"].update(byComp)
2068
2069        # portfolio distribution by sectors:
2070        view["analytics"]["distrBySectors"]["All money cash"] = {
2071            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2072            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2073        }
2074        view["analytics"]["distrBySectors"].update(bySect)
2075
2076        # portfolio distribution by currencies:
2077        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2078            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2079
2080            if self.moreDebug:
2081                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2082
2083        view["analytics"]["distrByCurrencies"].update(byCurr)
2084        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2085        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2086
2087        # portfolio distribution by countries:
2088        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2089            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2090
2091            if self.moreDebug:
2092                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2093
2094        view["analytics"]["distrByCountries"].update(byCountry)
2095        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2096        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2097
2098        # --- Prepare text statistics overview in human-readable:
2099        if show:
2100            actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)
2101
2102            # Whatever the value `details`, header not changes:
2103            info = [
2104                "# Client's portfolio\n\n",
2105                "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2106                "* **Account ID:** [{}]\n".format(self.accountId),
2107            ]
2108
2109            if details in ["full", "positions", "digest"]:
2110                info.extend([
2111                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2112                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2113                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2114                        view["stat"]["totalChangesRUB"],
2115                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2116                        view["stat"]["totalChangesPercentRUB"],
2117                    ),
2118                ])
2119
2120            if details in ["full", "positions"]:
2121                info.extend([
2122                    "## Open positions\n\n",
2123                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2124                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2125                    "| **Ruble:**                  | {:>31} |          |              |              |                     |                              |\n".format(
2126                        "{:.2f} ({:.2f}) rub".format(
2127                            view["stat"]["availableRUB"],
2128                            view["stat"]["blockedRUB"],
2129                        )
2130                    )
2131                ])
2132
2133                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2134                    return [
2135                        "|                             |                                 |          |              |              |                     |                              |\n",
2136                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2137                            noTradeStr if noTradeStr else typeStr,
2138                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2139                        ),
2140                    ]
2141
2142                def _InfoStr(data: dict, isCurr: bool = False) -> str:
2143                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2144                        "{} [{}]".format(data["ticker"], data["figi"]),
2145                        "{:.2f} ({:.2f}) {}".format(
2146                            data["volume"],
2147                            data["blocked"],
2148                            data["currency"],
2149                        ) if isCurr else "{:.0f} ({:.0f})".format(
2150                            data["volume"],
2151                            data["blocked"],
2152                        ),
2153                        "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."),
2154                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2155                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2156                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2157                        "{}{:.2f} {} ({}{:.2f}%)".format(
2158                            "+" if data["profit"] > 0 else "",
2159                            data["profit"], data["baseCurrencyName"],
2160                            "+" if data["percentProfit"] > 0 else "",
2161                            data["percentProfit"],
2162                        ),
2163                    )
2164
2165                # --- Show currencies section:
2166                if view["stat"]["Currencies"]:
2167                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2168                    for item in view["stat"]["Currencies"]:
2169                        info.append(_InfoStr(item, isCurr=True))
2170
2171                else:
2172                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2173
2174                # --- Show shares section:
2175                if view["stat"]["Shares"]:
2176                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2177
2178                    for item in view["stat"]["Shares"]:
2179                        info.append(_InfoStr(item))
2180
2181                else:
2182                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2183
2184                # --- Show bonds section:
2185                if view["stat"]["Bonds"]:
2186                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2187
2188                    for item in view["stat"]["Bonds"]:
2189                        info.append(_InfoStr(item))
2190
2191                else:
2192                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2193
2194                # --- Show etfs section:
2195                if view["stat"]["Etfs"]:
2196                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2197
2198                    for item in view["stat"]["Etfs"]:
2199                        info.append(_InfoStr(item))
2200
2201                else:
2202                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2203
2204                # --- Show futures section:
2205                if view["stat"]["Futures"]:
2206                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2207
2208                    for item in view["stat"]["Futures"]:
2209                        info.append(_InfoStr(item))
2210
2211                else:
2212                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2213
2214            if details in ["full", "orders"]:
2215                # --- Show pending limit orders section:
2216                if view["stat"]["orders"]:
2217                    info.extend([
2218                        "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])),
2219                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2220                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2221                    ])
2222
2223                    for item in view["stat"]["orders"]:
2224                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2225                            "{} [{}]".format(item["ticker"], item["figi"]),
2226                            item["orderID"],
2227                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2228                            "{} {} ({}{:.2f}%)".format(
2229                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2230                                item["baseCurrencyName"],
2231                                "+" if item["percentChanges"] > 0 else "",
2232                                float(item["percentChanges"]),
2233                            ),
2234                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2235                            item["action"],
2236                            item["type"],
2237                            item["date"],
2238                        ))
2239
2240                else:
2241                    info.append("\n## Total pending limit-orders: [0]\n")
2242
2243                # --- Show stop orders section:
2244                if view["stat"]["stopOrders"]:
2245                    info.extend([
2246                        "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])),
2247                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2248                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2249                    ])
2250
2251                    for item in view["stat"]["stopOrders"]:
2252                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2253                            "{} [{}]".format(item["ticker"], item["figi"]),
2254                            item["orderID"],
2255                            item["lotsRequested"],
2256                            "{} {} ({}{:.2f}%)".format(
2257                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2258                                item["baseCurrencyName"],
2259                                "+" if item["percentChanges"] > 0 else "",
2260                                float(item["percentChanges"]),
2261                            ),
2262                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2263                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2264                            item["action"],
2265                            item["type"],
2266                            item["expType"],
2267                            item["createDate"],
2268                            item["expDate"],
2269                        ))
2270
2271                else:
2272                    info.append("\n## Total stop-orders: [0]\n")
2273
2274            if details in ["full", "analytics"]:
2275                # -- Show analytics section:
2276                if view["stat"]["portfolioCostRUB"] > 0:
2277                    info.extend([
2278                        "\n# Analytics\n\n"
2279                        "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2280                        "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2281                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2282                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2283                            view["stat"]["totalChangesRUB"],
2284                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2285                            view["stat"]["totalChangesPercentRUB"],
2286                        ),
2287                        "\n## Portfolio distribution by assets\n"
2288                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2289                        "|------------------------------------|---------|---------|--------------------|\n",
2290                    ])
2291
2292                    for key in view["analytics"]["distrByAssets"].keys():
2293                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2294                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2295                                key,
2296                                view["analytics"]["distrByAssets"][key]["uniques"],
2297                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2298                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2299                            ))
2300
2301                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2302
2303                    info.extend([
2304                        "\n## Portfolio distribution by companies\n"
2305                        "\n| Company                                      | Percent | Current cost       |\n",
2306                        aSepLine,
2307                    ])
2308
2309                    for company in view["analytics"]["distrByCompanies"].keys():
2310                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2311                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2312                                "{}{}".format(
2313                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2314                                    company,
2315                                ),
2316                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2317                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2318                            ))
2319
2320                    info.extend([
2321                        "\n## Portfolio distribution by sectors\n"
2322                        "\n| Sector                                       | Percent | Current cost       |\n",
2323                        aSepLine,
2324                    ])
2325
2326                    for sector in view["analytics"]["distrBySectors"].keys():
2327                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2328                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2329                                sector,
2330                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2331                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2332                            ))
2333
2334                    info.extend([
2335                        "\n## Portfolio distribution by currencies\n"
2336                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2337                        aSepLine,
2338                    ])
2339
2340                    for curr in view["analytics"]["distrByCurrencies"].keys():
2341                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2342                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2343                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2344                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2345                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2346                            ))
2347
2348                    info.extend([
2349                        "\n## Portfolio distribution by countries\n"
2350                        "\n| Assets by country                            | Percent | Current cost       |\n",
2351                        aSepLine,
2352                    ])
2353
2354                    for country in view["analytics"]["distrByCountries"].keys():
2355                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2356                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2357                                country,
2358                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2359                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2360                            ))
2361
2362            if details in ["full", "calendar"]:
2363                # -- Show bonds payment calendar section:
2364                if view["stat"]["Bonds"]:
2365                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2366                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2367                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2368
2369                else:
2370                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2371
2372            infoText = "".join(info)
2373
2374            uLogger.info(infoText)
2375
2376            if details == "full" and self.overviewFile:
2377                filename = self.overviewFile
2378
2379            elif details == "digest" and self.overviewDigestFile:
2380                filename = self.overviewDigestFile
2381
2382            elif details == "positions" and self.overviewPositionsFile:
2383                filename = self.overviewPositionsFile
2384
2385            elif details == "orders" and self.overviewOrdersFile:
2386                filename = self.overviewOrdersFile
2387
2388            elif details == "analytics" and self.overviewAnalyticsFile:
2389                filename = self.overviewAnalyticsFile
2390
2391            elif details == "calendar" and self.overviewBondsCalendarFile:
2392                filename = self.overviewBondsCalendarFile
2393
2394            else:
2395                filename = ""
2396
2397            if filename:
2398                with open(filename, "w", encoding="UTF-8") as fH:
2399                    fH.write(infoText)
2400
2401                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2402
2403                if self.useHTMLReports:
2404                    htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html"
2405                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2406                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText))
2407
2408                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2409
2410        return view

Get portfolio: all open positions, orders and some statistics for current accountId. If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile and overviewBondsCalendarFile are defined then also save information to file.

WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.

Parameters
  • show: if False then only dictionary returns, if True then show more debug information.
  • details: how detailed should the information be?
    • full — shows full available information about portfolio status (by default),
    • positions — shows only open positions,
    • orders — shows only sections of open limits and stop orders.
    • digest — show a short digest of the portfolio status,
    • analytics — shows only the analytics section and the distribution of the portfolio by various categories,
    • calendar — shows only the bonds calendar section (if these present in portfolio),
Returns

dictionary with client's raw portfolio and some statistics.

def Deals( self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2412    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2413        """
2414        Returns history operations between two given dates for current `accountId`.
2415        If `reportFile` string is not empty then also save human-readable report.
2416        Shows some statistical data of closed positions.
2417
2418        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2419        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2420        :param show: if `True` then also prints all records to the console.
2421        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2422        :return: original list of dictionaries with history of deals records from API ("operations" key):
2423                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2424                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2425        """
2426        if self.accountId is None or not self.accountId:
2427            uLogger.error("Variable `accountId` must be defined for using this method!")
2428            raise Exception("Account ID required")
2429
2430        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2431
2432        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2433
2434        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2435        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2436        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2437        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2438        customStat = {}  # custom statistics in additional to responseJSON
2439
2440        # --- output report in human-readable format:
2441        if show or self.reportFile:
2442            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2443            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2444            nextDay = ""
2445
2446            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2447
2448            if len(ops) > 0:
2449                customStat = {
2450                    "opsCount": 0,  # total operations count
2451                    "buyCount": 0,  # buy operations
2452                    "sellCount": 0,  # sell operations
2453                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2454                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2455                    "payIn": {"rub": 0.},  # Deposit brokerage account
2456                    "payOut": {"rub": 0.},  # Withdrawals
2457                    "divs": {"rub": 0.},  # Dividends income
2458                    "coupons": {"rub": 0.},  # Coupon's income
2459                    "brokerCom": {"rub": 0.},  # Service commissions
2460                    "serviceCom": {"rub": 0.},  # Service commissions
2461                    "marginCom": {"rub": 0.},  # Margin commissions
2462                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2463                }
2464
2465                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2466                for item in ops:
2467                    if item["state"] == "OPERATION_STATE_EXECUTED":
2468                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2469
2470                        # count buy operations:
2471                        if "_BUY" in item["operationType"]:
2472                            customStat["buyCount"] += 1
2473
2474                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2475                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2476
2477                            else:
2478                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2479
2480                        # count sell operations:
2481                        elif "_SELL" in item["operationType"]:
2482                            customStat["sellCount"] += 1
2483
2484                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2485                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2486
2487                            else:
2488                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2489
2490                        # count incoming operations:
2491                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2492                            if item["payment"]["currency"] in customStat["payIn"].keys():
2493                                customStat["payIn"][item["payment"]["currency"]] += payment
2494
2495                            else:
2496                                customStat["payIn"][item["payment"]["currency"]] = payment
2497
2498                        # count withdrawals operations:
2499                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2500                            if item["payment"]["currency"] in customStat["payOut"].keys():
2501                                customStat["payOut"][item["payment"]["currency"]] += payment
2502
2503                            else:
2504                                customStat["payOut"][item["payment"]["currency"]] = payment
2505
2506                        # count dividends income:
2507                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2508                            if item["payment"]["currency"] in customStat["divs"].keys():
2509                                customStat["divs"][item["payment"]["currency"]] += payment
2510
2511                            else:
2512                                customStat["divs"][item["payment"]["currency"]] = payment
2513
2514                        # count coupon's income:
2515                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2516                            if item["payment"]["currency"] in customStat["coupons"].keys():
2517                                customStat["coupons"][item["payment"]["currency"]] += payment
2518
2519                            else:
2520                                customStat["coupons"][item["payment"]["currency"]] = payment
2521
2522                        # count broker commissions:
2523                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2524                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2525                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2526
2527                            else:
2528                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2529
2530                        # count service commissions:
2531                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2532                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2533                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2534
2535                            else:
2536                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2537
2538                        # count margin commissions:
2539                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2540                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2541                                customStat["marginCom"][item["payment"]["currency"]] += payment
2542
2543                            else:
2544                                customStat["marginCom"][item["payment"]["currency"]] = payment
2545
2546                        # count withholding taxes:
2547                        elif "_TAX" in item["operationType"]:
2548                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2549                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2550
2551                            else:
2552                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2553
2554                        else:
2555                            continue
2556
2557                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2558
2559                # --- view "Actions" lines:
2560                info.extend([
2561                    "| Report sections            |                               |                              |                      |                        |\n",
2562                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2563                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2564                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2565                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2566                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2567                    ),
2568                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2569                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2570                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2571                    ),
2572                ])
2573
2574                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2575                for key in opsKeys:
2576                    if key == "rub":
2577                        continue
2578
2579                    info.extend([
2580                        "|                            |                               | {:<28} |                      |                        |\n".format(
2581                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2582                        ),
2583                        "|                            |                               | {:<28} |                      |                        |\n".format(
2584                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2585                        ),
2586                    ])
2587
2588                info.append(splitLine1)
2589
2590                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2591                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2592                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2593                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2594                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2595                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2596                    )
2597
2598                # --- view "Payments" lines:
2599                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2600                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2601
2602                for key in paymentsKeys:
2603                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2604
2605                info.append(splitLine1)
2606
2607                # --- view "Commissions and taxes" lines:
2608                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2609                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2610
2611                for key in comKeys:
2612                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2613
2614                info.extend([
2615                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2616                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2617                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2618                ])
2619
2620            else:
2621                info.append("Broker returned no operations during this period\n")
2622
2623            # --- view "Operations" section:
2624            for item in ops:
2625                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2626                    continue
2627
2628                else:
2629                    self._figi = item["figi"]
2630                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2631                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2632
2633                    # group of deals during one day:
2634                    if nextDay and item["date"].split("T")[0] != nextDay:
2635                        info.append(splitLine2)
2636                        nextDay = ""
2637
2638                    else:
2639                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2640
2641                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2642                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2643                        self._figi if self._figi else "—",
2644                        instrument["ticker"] if instrument else "—",
2645                        instrument["type"] if instrument else "—",
2646                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2647                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2648                        TKS_OPERATION_STATES[item["state"]],
2649                        TKS_OPERATION_TYPES[item["operationType"]],
2650                    ))
2651
2652            infoText = "".join(info)
2653
2654            if show:
2655                if self.moreDebug:
2656                    uLogger.debug("Records about history of a client's operations successfully received")
2657
2658                uLogger.info(infoText)
2659
2660            if self.reportFile:
2661                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2662                    fH.write(infoText)
2663
2664                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2665
2666                if self.useHTMLReports:
2667                    htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html"
2668                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2669                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText))
2670
2671                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2672
2673        return ops, customStat

Returns history operations between two given dates for current accountId. If reportFile string is not empty then also save human-readable report. Shows some statistical data of closed positions.

Parameters
  • start: see docstring in TradeRoutines.GetDatesAsString() method.
  • end: see docstring in TradeRoutines.GetDatesAsString() method.
  • show: if True then also prints all records to the console.
  • showCancelled: if False then remove information about cancelled operations from the deals report.
Returns

original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.

def History( self, start: str = None, end: str = None, interval: str = 'hour', onlyMissing: bool = False, csvSep: str = ',', show: bool = False) -> pandas.core.frame.DataFrame:
2675    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2676        """
2677        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2678
2679        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2680        Warning! Broker server used ISO UTC time by default.
2681
2682        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2683        Also, `historyFile` used to update history with `onlyMissing` parameter.
2684
2685        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2686
2687        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2688        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2689        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2690                         `"hour"`, `"day"`. Default: `"hour"`.
2691        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2692                            False by default. Warning! History appends only from last candle to current time
2693                            with always update last candle!
2694        :param csvSep: separator if csv-file is used, `,` by default.
2695        :param show: if `True` then also prints Pandas DataFrame to the console.
2696        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2697                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2698        """
2699        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2700        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2701        history = None  # empty pandas object for history
2702
2703        if interval not in TKS_CANDLE_INTERVALS.keys():
2704            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2705            raise Exception("Incorrect value")
2706
2707        if not (self._ticker or self._figi):
2708            uLogger.error("Ticker or FIGI must be defined!")
2709            raise Exception("Ticker or FIGI required")
2710
2711        if self._ticker and not self._figi:
2712            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2713            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2714
2715        if self._figi and not self._ticker:
2716            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2717            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2718
2719        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2720        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2721        if interval.lower() != "day":
2722            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2723
2724        delta = dtEnd - dtStart  # current UTC time minus last time in file
2725        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2726
2727        # calculate history length in candles:
2728        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2729        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2730            length += 1  # to avoid fraction time
2731
2732        # calculate data blocks count:
2733        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2734
2735        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2736        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2737        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2738        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2739        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2740
2741        tempOld = None  # pandas object for old history, if --only-missing key present
2742        lastTime = None  # datetime object of last old candle in file
2743
2744        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2745            uLogger.debug("--only-missing key present, add only last missing candles...")
2746            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2747
2748            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2749
2750            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2751            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2752            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2753            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2754
2755            # get last datetime object from last string in file or minus 1 delta if file is empty:
2756            if len(tempOld) > 0:
2757                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2758
2759            else:
2760                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2761
2762            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2763
2764        responseJSONs = []  # raw history blocks of data
2765
2766        blockEnd = dtEnd
2767        for item in range(blocks):
2768            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2769            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2770
2771            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2772                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2773            ))
2774
2775            if blockStart == blockEnd:
2776                uLogger.debug("Skipped this zero-length block...")
2777
2778            else:
2779                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2780                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2781                self.body = str({
2782                    "figi": self._figi,
2783                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2784                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2785                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2786                })
2787                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2788
2789                if "code" in responseJSON.keys():
2790                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2791
2792                else:
2793                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2794                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2795
2796                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2797
2798            blockEnd = blockStart
2799
2800        printCount = len(responseJSONs)  # candles to show in console
2801        if responseJSONs:
2802            tempHistory = pd.DataFrame(
2803                data={
2804                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2805                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2806                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2807                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2808                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2809                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2810                    "volume": [int(item["volume"]) for item in responseJSONs],
2811                },
2812                index=range(len(responseJSONs)),
2813                columns=["date", "time", "open", "high", "low", "close", "volume"],
2814            )
2815            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2816            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2817
2818            # append only newest candles to old history if --only-missing key present:
2819            if onlyMissing and tempOld is not None and lastTime is not None:
2820                index = 0  # find start index in tempHistory data:
2821
2822                for i, item in tempHistory.iterrows():
2823                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2824
2825                    if curTime == lastTime:
2826                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2827                        index = i
2828                        printCount = index + 1
2829                        break
2830
2831                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2832
2833            else:
2834                history = tempHistory  # if no `--only-missing` key then load full data from server
2835
2836            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2837
2838        if history is not None and not history.empty:
2839            if show:
2840                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2841                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2842                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2843                ))
2844
2845        else:
2846            uLogger.warning("Received an empty candles history!")
2847
2848        if self.historyFile is not None:
2849            if history is not None and not history.empty:
2850                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2851                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2852
2853            else:
2854                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2855
2856        else:
2857            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2858
2859        return history

This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).

History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01. Warning! Broker server used ISO UTC time by default.

If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame. Also, historyFile used to update history with onlyMissing parameter.

See also: LoadHistory() and ShowHistoryChart() methods.

Parameters
  • start: see docstring in TradeRoutines.GetDatesAsString() method.
  • end: see docstring in TradeRoutines.GetDatesAsString() method.
  • interval: this is a candle interval. Current available values are "1min", "5min", "15min", "hour", "day". Default: "hour".
  • onlyMissing: if True then add only last missing candles, do not request all history length from start. False by default. Warning! History appends only from last candle to current time with always update last candle!
  • csvSep: separator if csv-file is used, , by default.
  • show: if True then also prints Pandas DataFrame to the console.
Returns

Pandas DataFrame with prices history. Headers of columns are defined by default: ["date", "time", "open", "high", "low", "close", "volume"].

def LoadHistory(self, filePath: str) -> pandas.core.frame.DataFrame:
2861    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2862        """
2863        Load candles history from csv-file and return Pandas DataFrame object.
2864
2865        See also: `History()` and `ShowHistoryChart()` methods.
2866
2867        :param filePath: path to csv-file to open.
2868        """
2869        loadedHistory = None  # init candles data object
2870
2871        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2872
2873        if os.path.exists(filePath):
2874            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2875
2876            tfStr = self.priceModel.FormattedDelta(
2877                self.priceModel.timeframe,
2878                "{days} days {hours}h {minutes}m {seconds}s",
2879            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2880                self.priceModel.timeframe,
2881                "{hours}h {minutes}m {seconds}s",
2882            )
2883
2884            if loadedHistory is not None and not loadedHistory.empty:
2885                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2886                    len(loadedHistory),
2887                    tfStr,
2888                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2889                )
2890
2891            else:
2892                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2893
2894        else:
2895            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2896
2897        return loadedHistory

Load candles history from csv-file and return Pandas DataFrame object.

See also: History() and ShowHistoryChart() methods.

Parameters
  • filePath: path to csv-file to open.
def ShowHistoryChart( self, candles: Union[str, pandas.core.frame.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2899    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2900        """
2901        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2902
2903        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2904        Default: `index.html` (both for interact and non-interact candlesticks chart).
2905
2906        See also: `History()` and `LoadHistory()` methods.
2907
2908        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2909        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2910                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2911                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2912                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2913        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2914                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2915        """
2916        if isinstance(candles, str):
2917            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2918            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2919
2920        elif isinstance(candles, pd.DataFrame):
2921            self.priceModel.prices = candles  # set candles chain from variable
2922            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2923
2924            if "datetime" not in candles.columns:
2925                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2926
2927        else:
2928            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2929            raise Exception("Incorrect value")
2930
2931        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2932
2933        if interact:
2934            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2935
2936            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2937
2938        else:
2939            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2940
2941            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2942
2943        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))

Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.

Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart. Default: index.html (both for interact and non-interact candlesticks chart).

See also: History() and LoadHistory() methods.

Parameters
def Trade( self, operation: str, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2945    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2946        """
2947        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2948        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2949
2950        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2951
2952        :param operation: string "Buy" or "Sell".
2953        :param lots: volume, integer count of lots >= 1.
2954        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2955        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2956        :param expDate: string "Undefined" by default or local date in future,
2957                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2958        :return: JSON with response from broker server.
2959        """
2960        if self.accountId is None or not self.accountId:
2961            uLogger.error("Variable `accountId` must be defined for using this method!")
2962            raise Exception("Account ID required")
2963
2964        if operation is None or not operation or operation not in ("Buy", "Sell"):
2965            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2966            raise Exception("Incorrect value")
2967
2968        if lots is None or lots < 1:
2969            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2970            lots = 1
2971
2972        if tp is None or tp < 0:
2973            tp = 0
2974
2975        if sl is None or sl < 0:
2976            sl = 0
2977
2978        if expDate is None or not expDate:
2979            expDate = "Undefined"
2980
2981        if not (self._ticker or self._figi):
2982            uLogger.error("Ticker or FIGI must be defined!")
2983            raise Exception("Ticker or FIGI required")
2984
2985        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
2986        self._ticker = instrument["ticker"]
2987        self._figi = instrument["figi"]
2988
2989        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
2990
2991        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2992        self.body = str({
2993            "figi": self._figi,
2994            "quantity": str(lots),
2995            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2996            "accountId": str(self.accountId),
2997            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2998        })
2999        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
3000
3001        if "orderId" in response.keys():
3002            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
3003                operation, response["orderId"],
3004                self._ticker, self._figi, lots,
3005                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
3006                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
3007                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
3008            ))
3009
3010            if tp > 0:
3011                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
3012
3013            if sl > 0:
3014                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3015
3016        else:
3017            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
3018
3019        return response

Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().

Parameters
  • operation: string "Buy" or "Sell".
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter targetPrice in self.Order().
  • sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter targetPrice in self.Order().
  • expDate: string "Undefined" by default or local date in future, it is a string with format %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Buy( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
3021    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3022        """
3023        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3024        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3025
3026        See also: `Order()` and `Trade()` docstrings.
3027
3028        :param lots: volume, integer count of lots >= 1.
3029        :param tp: float > 0, take profit price of stop-order.
3030        :param sl: float > 0, stop loss price of stop-order.
3031        :param expDate: it's a local date in future.
3032                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3033        :return: JSON with response from broker server.
3034        """
3035        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Sell( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
3037    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3038        """
3039        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3040        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3041
3042        See also: `Order()` and `Trade()` docstrings.
3043
3044        :param lots: volume, integer count of lots >= 1.
3045        :param tp: float > 0, take profit price of stop-order.
3046        :param sl: float > 0, stop loss price of stop-order.
3047        :param expDate: it's a local date in the future.
3048                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3049        :return: JSON with response from broker server.
3050        """
3051        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in the future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3053    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3054        """
3055        Close position of given instruments.
3056
3057        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3058        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3059                         This avoids unnecessary downloading data from the server.
3060        """
3061        if instruments is None or not instruments:
3062            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3063            raise Exception("Ticker or FIGI required")
3064
3065        if isinstance(instruments, str):
3066            instruments = [instruments]
3067
3068        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3069        if uniqueInstruments:
3070            if portfolio is None or not portfolio:
3071                portfolio = self.Overview(show=False)
3072
3073            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3074            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3075
3076            for self._figi in uniqueInstruments:
3077                if self._figi not in allOpened:
3078                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3079                    continue
3080
3081                # search open trade info about instrument by ticker:
3082                instrument = {}
3083                for iType in TKS_INSTRUMENTS:
3084                    if instrument:
3085                        break
3086
3087                    for item in portfolio["stat"][iType]:
3088                        if item["figi"] == self._figi:
3089                            instrument = item
3090                            break
3091
3092                if instrument:
3093                    self._ticker = instrument["ticker"]
3094                    self._figi = instrument["figi"]
3095
3096                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3097                        self._ticker,
3098                        self._figi,
3099                        int(instrument["volume"]),
3100                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3101                    ))
3102
3103                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3104
3105                    if tradeLots > 0:
3106                        if instrument["blocked"] > 0:
3107                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3108                                instrument["blocked"],
3109                                self._ticker,
3110                                tradeLots,
3111                            ))
3112
3113                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3114                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3115
3116                    else:
3117                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))

Close position of given instruments.

Parameters
  • instruments: list of instruments defined by tickers or FIGIs that must be closed.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3119    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3120        """
3121        Close all positions of given instruments with defined type.
3122
3123        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3124        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3125                         This avoids unnecessary downloading data from the server.
3126        """
3127        if iType not in TKS_INSTRUMENTS:
3128            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3129
3130        else:
3131            if portfolio is None or not portfolio:
3132                portfolio = self.Overview(show=False)
3133
3134            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3135            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3136
3137            if tickers and portfolio:
3138                self.CloseTrades(tickers, portfolio)
3139
3140            else:
3141                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))

Close all positions of given instruments with defined type.

Parameters
  • iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def Order( self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3143    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3144        """
3145        Universal method to create market or limit orders with all available parameters for current `accountId`.
3146        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3147
3148        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3149        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3150
3151        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3152        then broker immediately open market order as you can do simple --buy or --sell operations!
3153
3154        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3155        When current price will go up or down to target price value then broker opens a limit order.
3156        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3157
3158        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3159
3160        :param operation: string "Buy" or "Sell".
3161        :param orderType: string "Limit" or "Stop".
3162        :param lots: volume, integer count of lots >= 1.
3163        :param targetPrice: target price > 0. This is open trade price for limit order.
3164        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3165                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3166        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3167                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3168                         Stop loss order always executed by market price.
3169        :param expDate: string "Undefined" by default or local date in future.
3170                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3171                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3172                        A limit order has no expiration date, it lasts until the end of the trading day.
3173        :return: JSON with response from broker server.
3174        """
3175        if self.accountId is None or not self.accountId:
3176            uLogger.error("Variable `accountId` must be defined for using this method!")
3177            raise Exception("Account ID required")
3178
3179        if operation is None or not operation or operation not in ("Buy", "Sell"):
3180            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3181            raise Exception("Incorrect value")
3182
3183        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3184            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3185            raise Exception("Incorrect value")
3186
3187        if lots is None or lots < 1:
3188            uLogger.error("You must define trade volume > 0: integer count of lots!")
3189            raise Exception("Incorrect value")
3190
3191        if targetPrice is None or targetPrice <= 0:
3192            uLogger.error("Target price for limit-order must be greater than 0!")
3193            raise Exception("Incorrect value")
3194
3195        if limitPrice is None or limitPrice <= 0:
3196            limitPrice = targetPrice
3197
3198        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3199            stopType = "Limit"
3200
3201        if expDate is None or not expDate:
3202            expDate = "Undefined"
3203
3204        if not (self._ticker or self._figi):
3205            uLogger.error("Tocker or FIGI must be defined!")
3206            raise Exception("Ticker or FIGI required")
3207
3208        response = {}
3209        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3210        self._ticker = instrument["ticker"]
3211        self._figi = instrument["figi"]
3212
3213        if orderType == "Limit":
3214            uLogger.debug(
3215                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3216                    self._ticker, self._figi,
3217                    operation, lots, targetPrice, instrument["currency"],
3218                ))
3219
3220            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3221            self.body = str({
3222                "figi": self._figi,
3223                "quantity": str(lots),
3224                "price": FloatToNano(targetPrice),
3225                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3226                "accountId": str(self.accountId),
3227                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3228            })
3229            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3230
3231            if "orderId" in response.keys():
3232                uLogger.info(
3233                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format(
3234                        response["orderId"], self._ticker, self._figi, operation, lots,
3235                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3236                    ))
3237
3238                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3239                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3240                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3241                            targetPrice, instrument["currency"],
3242                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3243                        ))
3244
3245                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3246                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3247                            targetPrice, instrument["currency"],
3248                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3249                        ))
3250
3251            else:
3252                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3253
3254        if orderType == "Stop":
3255            uLogger.debug(
3256                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3257                    self._ticker, self._figi,
3258                    operation, lots,
3259                    targetPrice, instrument["currency"],
3260                    limitPrice, instrument["currency"],
3261                    stopType, expDate,
3262                ))
3263
3264            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3265            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3266            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3267
3268            body = {
3269                "figi": self._figi,
3270                "quantity": str(lots),
3271                "price": FloatToNano(limitPrice),
3272                "stopPrice": FloatToNano(targetPrice),
3273                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3274                "accountId": str(self.accountId),
3275                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3276                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3277            }
3278
3279            if expDateUTC:
3280                body["expireDate"] = expDateUTC
3281
3282            self.body = str(body)
3283            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3284
3285            if "stopOrderId" in response.keys():
3286                uLogger.info(
3287                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format(
3288                        response["stopOrderId"], self._ticker, self._figi, operation, lots,
3289                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3290                        "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"],
3291                        TKS_STOP_ORDER_TYPES[stopOrderType],
3292                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3293                    ))
3294
3295                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3296                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3297                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3298                            targetPrice, instrument["currency"],
3299                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3300                        ))
3301
3302                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3303                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3304                            targetPrice, instrument["currency"],
3305                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3306                        ))
3307
3308            else:
3309                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3310
3311        return response

Universal method to create market or limit orders with all available parameters for current accountId. See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().

If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.

Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!

If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.

Only one attempt and no retry for opens order. If network issue occurred you can create new request.

Parameters
  • operation: string "Buy" or "Sell".
  • orderType: string "Limit" or "Stop".
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
  • limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
  • stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns

JSON with response from broker server.

def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3313    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3314        """
3315        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3316        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3317        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3318        See also: `Order()` docstring.
3319
3320        :param lots: volume, integer count of lots >= 1.
3321        :param targetPrice: target price > 0. This is open trade price for limit order.
3322        :return: JSON with response from broker server.
3323        """
3324        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Buy limit-order (below current price). You must specify only 2 parameters: lots and target price to open buy limit-order. If you try to create buy limit-order above current price then broker immediately open Buy market order, such as if you do simple --buy operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def BuyStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3326    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3327        """
3328        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3329        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3330        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3331        target price value then broker opens a limit order. See also: `Order()` docstring.
3332
3333        :param lots: volume, integer count of lots >= 1.
3334        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3335        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3336                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3337        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3338                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3339        :param expDate: string "Undefined" by default or local date in future.
3340                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3341                        This date is converting to UTC format for server.
3342        :return: JSON with response from broker server.
3343        """
3344        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order. In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for buy stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def SellLimit(self, lots: int, targetPrice: float) -> dict:
3346    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3347        """
3348        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3349        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3350        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3351        See also: `Order()` docstring.
3352
3353        :param lots: volume, integer count of lots >= 1.
3354        :param targetPrice: target price > 0. This is open trade price for limit order.
3355        :return: JSON with response from broker server.
3356        """
3357        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Sell limit-order (above current price). You must specify only 2 parameters: lots and target price to open sell limit-order. If you try to create sell limit-order below current price then broker immediately open Sell market order, such as if you do simple --sell operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def SellStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3359    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3360        """
3361        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3362        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3363        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3364        target price value then broker opens a limit order. See also: `Order()` docstring.
3365
3366        :param lots: volume, integer count of lots >= 1.
3367        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3368        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3369                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3370        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3371                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3372        :param expDate: string "Undefined" by default or local date in future.
3373                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3374                        This date is converting to UTC format for server.
3375        :return: JSON with response from broker server.
3376        """
3377        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order. In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for sell stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def CloseOrders( self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3379    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3380        """
3381        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3382
3383        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3384        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3385                             This avoids unnecessary downloading data from the server.
3386        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3387        """
3388        if self.accountId is None or not self.accountId:
3389            uLogger.error("Variable `accountId` must be defined for using this method!")
3390            raise Exception("Account ID required")
3391
3392        if orderIDs:
3393            if allOrdersIDs is None:
3394                rawOrders = self.RequestPendingOrders()
3395                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3396
3397            if allStopOrdersIDs is None:
3398                rawStopOrders = self.RequestStopOrders()
3399                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3400
3401            for orderID in orderIDs:
3402                idInPendingOrders = orderID in allOrdersIDs
3403                idInStopOrders = orderID in allStopOrdersIDs
3404
3405                if not (idInPendingOrders or idInStopOrders):
3406                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3407                    continue
3408
3409                else:
3410                    if idInPendingOrders:
3411                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3412
3413                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3414                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3415                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3416                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3417
3418                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3419                            if self.moreDebug:
3420                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3421
3422                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3423
3424                        else:
3425                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3426
3427                    elif idInStopOrders:
3428                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3429
3430                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3431                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3432                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3433                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3434
3435                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3436                            if self.moreDebug:
3437                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3438
3439                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3440
3441                        else:
3442                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3443
3444                    else:
3445                        continue

Cancel order or list of orders by its orderId or stopOrderId for current accountId.

Parameters
  • orderIDs: list of integers with orderId or stopOrderId.
  • allOrdersIDs: pre-received lists of all active pending limit orders. This avoids unnecessary downloading data from the server.
  • allStopOrdersIDs: pre-received lists of all active stop orders.
def CloseAllOrders(self) -> None:
3447    def CloseAllOrders(self) -> None:
3448        """
3449        Gets a list of open pending and stop orders and cancel it all.
3450        """
3451        rawOrders = self.RequestPendingOrders()
3452        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3453        lenOrders = len(allOrdersIDs)
3454
3455        rawStopOrders = self.RequestStopOrders()
3456        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3457        lenSOrders = len(allStopOrdersIDs)
3458
3459        if lenOrders > 0 or lenSOrders > 0:
3460            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3461
3462            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3463
3464        else:
3465            uLogger.info("Orders not found, nothing to cancel.")

Gets a list of open pending and stop orders and cancel it all.

def CloseAll(self, *args) -> None:
3467    def CloseAll(self, *args) -> None:
3468        """
3469        Close all available (not blocked) opened trades and orders.
3470
3471        Also, you can select one or more keywords case-insensitive:
3472        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3473
3474        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3475        """
3476        overview = self.Overview(show=False)  # get all open trades info
3477
3478        if len(args) == 0:
3479            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3480            self.CloseAllOrders()  # close all pending and stop orders
3481
3482            for iType in TKS_INSTRUMENTS:
3483                if iType != "Currencies":
3484                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3485
3486        else:
3487            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3488            lowerArgs = [x.lower() for x in args]
3489
3490            if "orders" in lowerArgs:
3491                self.CloseAllOrders()  # close all pending and stop orders
3492
3493            for iType in TKS_INSTRUMENTS:
3494                if iType.lower() in lowerArgs and iType != "Currencies":
3495                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies

Close all available (not blocked) opened trades and orders.

Also, you can select one or more keywords case-insensitive: orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.

Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.

def CloseAllByTicker(self, instrument: str) -> None:
3497    def CloseAllByTicker(self, instrument: str) -> None:
3498        """
3499        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3500
3501        This method searches opened trade and orders of instrument throw all portfolio and then use
3502        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3503
3504        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3505
3506        :param instrument: string with ticker.
3507        """
3508        if instrument is None or not instrument:
3509            uLogger.error("Ticker name must be defined for using this method!")
3510            raise Exception("Ticker required")
3511
3512        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3513
3514        self._ticker = instrument  # try to set instrument as ticker
3515        self._figi = ""
3516
3517        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3518        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3519
3520        if limitAll and self.IsInLimitOrders(portfolio=overview):
3521            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3522            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3523
3524        if stopAll and self.IsInStopOrders(portfolio=overview):
3525            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3526            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3527
3528        if self.IsInPortfolio(portfolio=overview):
3529            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3530            self.CloseTrades(instruments=[instrument], portfolio=overview)

Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.

This method searches opened trade and orders of instrument throw all portfolio and then use CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.

See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().

Parameters
  • instrument: string with ticker.
def CloseAllByFIGI(self, instrument: str) -> None:
3532    def CloseAllByFIGI(self, instrument: str) -> None:
3533        """
3534        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3535
3536        This method searches opened trade and orders of instrument throw all portfolio and then use
3537        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3538
3539        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3540
3541        :param instrument: string with FIGI id.
3542        """
3543        if instrument is None or not instrument:
3544            uLogger.error("FIGI id must be defined for using this method!")
3545            raise Exception("FIGI required")
3546
3547        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3548
3549        self._ticker = ""
3550        self._figi = instrument  # try to set instrument as FIGI id
3551
3552        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3553        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3554
3555        if limitAll and self.IsInLimitOrders(portfolio=overview):
3556            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3557            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3558
3559        if stopAll and self.IsInStopOrders(portfolio=overview):
3560            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3561            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3562
3563        if self.IsInPortfolio(portfolio=overview):
3564            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3565            self.CloseTrades(instruments=[instrument], portfolio=overview)

Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.

This method searches opened trade and orders of instrument throw all portfolio and then use CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.

See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().

Parameters
  • instrument: string with FIGI id.
@staticmethod
def ParseOrderParameters(operation, **inputParameters):
3567    @staticmethod
3568    def ParseOrderParameters(operation, **inputParameters):
3569        """
3570        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3571
3572        :param operation: string "Buy" or "Sell".
3573        :param inputParameters: this is dict of strings that looks like this
3574               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3575               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3576               "prices" key: one or more prices to open limit-orders
3577               Counts of values in lots and prices lists must be equals!
3578        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3579        """
3580        # TODO: update order grid work with api v2
3581        pass
3582        # uLogger.debug("Input parameters: {}".format(inputParameters))
3583        #
3584        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3585        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3586        #     raise Exception("Incorrect value")
3587        #
3588        # if "l" in inputParameters.keys():
3589        #     inputParameters["lots"] = inputParameters.pop("l")
3590        #
3591        # if "p" in inputParameters.keys():
3592        #     inputParameters["prices"] = inputParameters.pop("p")
3593        #
3594        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3595        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3596        #     raise Exception("Incorrect value")
3597        #
3598        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3599        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3600        #
3601        # if len(lots) != len(prices):
3602        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3603        #     raise Exception("Incorrect value")
3604        #
3605        # uLogger.debug("Extracted parameters for orders:")
3606        # uLogger.debug("lots = {}".format(lots))
3607        # uLogger.debug("prices = {}".format(prices))
3608        #
3609        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3610        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3611        # uLogger.debug("Order parameters: {}".format(result))
3612        #
3613        # return result

Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.

Parameters
  • operation: string "Buy" or "Sell".
  • inputParameters: this is dict of strings that looks like this {"lots": "L_int,...", "prices": "P_float,..."} where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns

list of dictionaries with all lots and prices to open orders that looks like this [{"lot": lots_1, "price": price_1}, {...}, ...]

def IsInPortfolio(self, portfolio: dict = None) -> bool:
3615    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3616        """
3617        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3618
3619        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3620        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3621        """
3622        result = False
3623        msg = "Instrument not defined!"
3624
3625        if portfolio is None or not portfolio:
3626            portfolio = self.Overview(show=False)
3627
3628        if self._ticker:
3629            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3630            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3631
3632            for iType in TKS_INSTRUMENTS:
3633                for instrument in portfolio["stat"][iType]:
3634                    if instrument["ticker"] == self._ticker:
3635                        result = True
3636                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3637                        break
3638
3639        elif self._figi:
3640            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3641            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3642
3643            for iType in TKS_INSTRUMENTS:
3644                for instrument in portfolio["stat"][iType]:
3645                    if instrument["figi"] == self._figi:
3646                        result = True
3647                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3648                        break
3649
3650        else:
3651            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3652
3653        uLogger.debug(msg)
3654
3655        return result

Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if portfolio contains open position with given instrument, False otherwise.

def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3657    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3658        """
3659        Returns instrument from the user's portfolio if it presents there.
3660        Instrument must be defined by `ticker` (highly priority) or `figi`.
3661
3662        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3663        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3664        """
3665        result = None
3666        msg = "Instrument not defined!"
3667
3668        if portfolio is None or not portfolio:
3669            portfolio = self.Overview(show=False)
3670
3671        if self._ticker:
3672            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3673            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3674
3675            for iType in TKS_INSTRUMENTS:
3676                for instrument in portfolio["stat"][iType]:
3677                    if instrument["ticker"] == self._ticker:
3678                        result = instrument
3679                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3680                        break
3681
3682        elif self._figi:
3683            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3684            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3685
3686            for iType in TKS_INSTRUMENTS:
3687                for instrument in portfolio["stat"][iType]:
3688                    if instrument["figi"] == self._figi:
3689                        result = instrument
3690                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3691                        break
3692
3693        else:
3694            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3695
3696        uLogger.debug(msg)
3697
3698        return result

Returns instrument from the user's portfolio if it presents there. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

dict with instrument if portfolio contains open position with this instrument, None otherwise.

def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3700    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3701        """
3702        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3703
3704        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3705
3706        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3707        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3708        """
3709        result = False
3710        msg = "Instrument not defined!"
3711
3712        if portfolio is None or not portfolio:
3713            portfolio = self.Overview(show=False)
3714
3715        if self._ticker:
3716            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3717            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3718
3719            for instrument in portfolio["stat"]["orders"]:
3720                if instrument["ticker"] == self._ticker:
3721                    result = True
3722                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3723                    break
3724
3725        elif self._figi:
3726            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3727            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3728
3729            for instrument in portfolio["stat"]["orders"]:
3730                if instrument["figi"] == self._figi:
3731                    result = True
3732                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3733                    break
3734
3735        else:
3736            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3737
3738        uLogger.debug(msg)
3739
3740        return result

Checks if instrument is in the limit orders list. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if limit orders list contains some limit orders for the instrument, False otherwise.

def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3742    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3743        """
3744        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3745        Instrument must be defined by `ticker` (highly priority) or `figi`.
3746
3747        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3748
3749        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3750        :return: list with `orderID`s of limit orders.
3751        """
3752        result = []
3753        msg = "Instrument not defined!"
3754
3755        if portfolio is None or not portfolio:
3756            portfolio = self.Overview(show=False)
3757
3758        if self._ticker:
3759            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3760            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3761
3762            for instrument in portfolio["stat"]["orders"]:
3763                if instrument["ticker"] == self._ticker:
3764                    result.append(instrument["orderID"])
3765
3766            if result:
3767                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3768
3769        elif self._figi:
3770            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3771            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3772
3773            for instrument in portfolio["stat"]["orders"]:
3774                if instrument["figi"] == self._figi:
3775                    result.append(instrument["orderID"])
3776
3777            if result:
3778                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3779
3780        else:
3781            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3782
3783        uLogger.debug(msg)
3784
3785        return result

Returns list with all orderIDs of opened pending limit orders for the instrument. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

list with orderIDs of limit orders.

def IsInStopOrders(self, portfolio: dict = None) -> bool:
3787    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3788        """
3789        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3790
3791        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3792
3793        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3794        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3795        """
3796        result = False
3797        msg = "Instrument not defined!"
3798
3799        if portfolio is None or not portfolio:
3800            portfolio = self.Overview(show=False)
3801
3802        if self._ticker:
3803            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3804            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3805
3806            for instrument in portfolio["stat"]["stopOrders"]:
3807                if instrument["ticker"] == self._ticker:
3808                    result = True
3809                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3810                    break
3811
3812        elif self._figi:
3813            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3814            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3815
3816            for instrument in portfolio["stat"]["stopOrders"]:
3817                if instrument["figi"] == self._figi:
3818                    result = True
3819                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3820                    break
3821
3822        else:
3823            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3824
3825        uLogger.debug(msg)
3826
3827        return result

Checks if instrument is in the stop orders list. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if stop orders list contains some stop orders for the instrument, False otherwise.

def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3829    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3830        """
3831        Returns list with all `orderID`s of opened stop orders for the instrument.
3832        Instrument must be defined by `ticker` (highly priority) or `figi`.
3833
3834        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3835
3836        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3837        :return: list with `orderID`s of stop orders.
3838        """
3839        result = []
3840        msg = "Instrument not defined!"
3841
3842        if portfolio is None or not portfolio:
3843            portfolio = self.Overview(show=False)
3844
3845        if self._ticker:
3846            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3847            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3848
3849            for instrument in portfolio["stat"]["stopOrders"]:
3850                if instrument["ticker"] == self._ticker:
3851                    result.append(instrument["orderID"])
3852
3853            if result:
3854                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3855
3856        elif self._figi:
3857            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3858            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3859
3860            for instrument in portfolio["stat"]["stopOrders"]:
3861                if instrument["figi"] == self._figi:
3862                    result.append(instrument["orderID"])
3863
3864            if result:
3865                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3866
3867        else:
3868            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3869
3870        uLogger.debug(msg)
3871
3872        return result

Returns list with all orderIDs of opened stop orders for the instrument. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

list with orderIDs of stop orders.

def RequestLimits(self) -> dict:
3874    def RequestLimits(self) -> dict:
3875        """
3876        Method for obtaining the available funds for withdrawal for current `accountId`.
3877
3878        See also:
3879        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3880        - `OverviewLimits()` method
3881
3882        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3883                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3884                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3885                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3886        """
3887        if self.accountId is None or not self.accountId:
3888            uLogger.error("Variable `accountId` must be defined for using this method!")
3889            raise Exception("Account ID required")
3890
3891        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3892
3893        self.body = str({"accountId": self.accountId})
3894        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3895        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3896
3897        if self.moreDebug:
3898            uLogger.debug("Records about available funds for withdrawal successfully received")
3899
3900        return rawLimits

Method for obtaining the available funds for withdrawal for current accountId.

See also:

Returns

dict with raw data from server that contains free funds for withdrawal. Example of dict: {"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Here money is an array of portfolio currency positions, blocked is an array of blocked currency positions of the portfolio and blockedGuarantee is locked money under collateral for futures.

def OverviewLimits(self, show: bool = False) -> dict:
3902    def OverviewLimits(self, show: bool = False) -> dict:
3903        """
3904        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3905
3906        See also: `RequestLimits()`.
3907
3908        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3909        :return: dict with raw parsed data from server and some calculated statistics about it.
3910        """
3911        if self.accountId is None or not self.accountId:
3912            uLogger.error("Variable `accountId` must be defined for using this method!")
3913            raise Exception("Account ID required")
3914
3915        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3916
3917        view = {
3918            "rawLimits": rawLimits,
3919            "limits": {  # parsed data for every currency:
3920                "money": {  # this is an array of portfolio currency positions
3921                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3922                },
3923                "blocked": {  # this is an array of blocked currency
3924                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3925                },
3926                "blockedGuarantee": {  # this is locked money under collateral for futures
3927                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3928                },
3929            },
3930        }
3931
3932        # --- Prepare text table with limits in human-readable format:
3933        if show:
3934            info = [
3935                "# Withdrawal limits\n\n",
3936                "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3937                "* **Account ID:** [{}]\n".format(self.accountId),
3938            ]
3939
3940            if view["limits"]["money"]:
3941                info.extend([
3942                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3943                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3944                ])
3945
3946            else:
3947                info.append("\nNo withdrawal limits\n")
3948
3949            for curr in view["limits"]["money"].keys():
3950                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3951                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3952                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3953
3954                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3955                    "[{}]".format(curr),
3956                    "{:.2f}".format(view["limits"]["money"][curr]),
3957                    "{:.2f}".format(availableMoney),
3958                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3959                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3960                )
3961
3962                if curr == "rub":
3963                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3964
3965                else:
3966                    info.append(infoStr)
3967
3968            infoText = "".join(info)
3969
3970            uLogger.info(infoText)
3971
3972            if self.withdrawalLimitsFile:
3973                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3974                    fH.write(infoText)
3975
3976                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3977
3978                if self.useHTMLReports:
3979                    htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html"
3980                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
3981                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText))
3982
3983                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
3984
3985        return view

Method for parsing and show table with available funds for withdrawal for current accountId.

See also: RequestLimits().

Parameters
  • show: if False then only dictionary returns, if True then also print withdrawal limits to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

def RequestAccounts(self) -> dict:
3987    def RequestAccounts(self) -> dict:
3988        """
3989        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3990
3991        See also:
3992        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3993        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3994        - `OverviewUserInfo()` method
3995
3996        :return: dict with raw data from server that contains accounts info. Example of dict:
3997                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3998                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3999                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
4000                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
4001        """
4002        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
4003
4004        self.body = str({})
4005        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
4006        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
4007
4008        if self.moreDebug:
4009            uLogger.debug("Records about available accounts successfully received")
4010
4011        return rawAccounts

Method for requesting all brokerage accounts (accountIds) of current user detected by token.

See also:

Returns

dict with raw data from server that contains accounts info. Example of dict: {"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. If closedDate="1970-01-01T00:00:00Z" it means that account is active now.

def RequestUserInfo(self) -> dict:
4013    def RequestUserInfo(self) -> dict:
4014        """
4015        Method for requesting common user's information.
4016
4017        See also:
4018        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
4019        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
4020        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
4021        - `OverviewUserInfo()` method
4022
4023        :return: dict with raw data from server that contains user's information. Example of dict:
4024                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
4025                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
4026        """
4027        uLogger.debug("Requesting common user's information. Wait, please...")
4028
4029        self.body = str({})
4030        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
4031        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
4032
4033        if self.moreDebug:
4034            uLogger.debug("Records about current user successfully received")
4035
4036        return rawUserInfo

Method for requesting common user's information.

See also:

Returns

dict with raw data from server that contains user's information. Example of dict: {"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.

def RequestMarginStatus(self, accountId: str = None) -> dict:
4038    def RequestMarginStatus(self, accountId: str = None) -> dict:
4039        """
4040        Method for requesting margin calculation for defined account ID.
4041
4042        See also:
4043        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
4044        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
4045        - `OverviewUserInfo()` method
4046
4047        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
4048        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
4049                 Example of responses:
4050                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
4051                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
4052                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
4053                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
4054                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
4055                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
4056        """
4057        if accountId is None or not accountId:
4058            if self.accountId is None or not self.accountId:
4059                uLogger.error("Variable `accountId` must be defined for using this method!")
4060                raise Exception("Account ID required")
4061
4062            else:
4063                accountId = self.accountId  # use `self.accountId` (main ID) by default
4064
4065        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
4066
4067        self.body = str({"accountId": accountId})
4068        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
4069        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
4070
4071        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
4072            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
4073            rawMargin = {}
4074
4075        else:
4076            if self.moreDebug:
4077                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
4078
4079        return rawMargin

Method for requesting margin calculation for defined account ID.

See also:

Parameters
  • accountId: string with numeric account ID. If None, then used class field accountId.
Returns

dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400: {"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns: {}. status code 200: {"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.

def RequestTariffLimits(self) -> dict:
4081    def RequestTariffLimits(self) -> dict:
4082        """
4083        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4084
4085        See also:
4086        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4087        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4088        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4089        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4090        - `OverviewUserInfo()` method
4091
4092        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4093                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4094                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4095        """
4096        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4097
4098        self.body = str({})
4099        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4100        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4101
4102        if self.moreDebug:
4103            uLogger.debug("Records with limits of current tariff successfully received")
4104
4105        return rawTariffLimits

Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.

See also:

Returns

dict with raw data from server that contains limits of current tariff. Example of dict: {"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.

def RequestBondCoupons(self, iJSON: dict) -> dict:
4107    def RequestBondCoupons(self, iJSON: dict) -> dict:
4108        """
4109        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4110        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4111        All dates are in UTC timezone.
4112
4113        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4114        Documentation:
4115        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4116        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4117
4118        See also: `ExtendBondsData()`.
4119
4120        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4121                      If raw iJSON is not data of bond then server returns an error [400] with message:
4122                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4123        :return: dictionary with bond payment calendar. Response example
4124                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4125                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4126                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4127                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4128        """
4129        if iJSON["figi"] is None or not iJSON["figi"]:
4130            uLogger.error("FIGI must be defined for using this method!")
4131            raise Exception("FIGI required")
4132
4133        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4134        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4135
4136        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4137            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4138            self._figi,
4139            startDate,
4140            endDate,
4141        ))
4142
4143        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4144        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4145        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4146
4147        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4148            uLogger.warning("Instrument type is not bond!")
4149
4150        else:
4151            if self.moreDebug:
4152                uLogger.debug("Records about bond payment calendar successfully received")
4153
4154        return calendar

Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z". All dates are in UTC timezone.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:

See also: ExtendBondsData().

Parameters
  • iJSON: raw json data of a bond from broker server, example iJSON = self.iList["Bonds"][self._ticker] If raw iJSON is not data of bond then server returns an error [400] with message: {"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns

dictionary with bond payment calendar. Response example {"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}

def ExtendBondsData( self, instruments: list[str], xlsx: bool = False) -> pandas.core.frame.DataFrame:
4156    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4157        """
4158        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4159        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4160        coupon yields, current yields and some statistics etc.
4161
4162        WARNING! This is too long operation if a lot of bonds requested from broker server.
4163
4164        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4165
4166        :param instruments: list of strings with tickers or FIGIs.
4167        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4168                     for further used by data scientists or stock analytics.
4169        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4170                 In XLSX-file and Pandas DataFrame fields mean:
4171                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4172                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4173        """
4174        if instruments is None or not instruments:
4175            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4176            raise Exception("Ticker or FIGI required")
4177
4178        if isinstance(instruments, str):
4179            instruments = [instruments]
4180
4181        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4182
4183        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4184
4185        iCount = len(uniqueInstruments)
4186        tooLong = iCount >= 20
4187        if tooLong:
4188            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4189
4190        bonds = None
4191        for i, self._figi in enumerate(uniqueInstruments):
4192            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4193
4194            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4195                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4196                rawBond = self.SearchByFIGI(requestPrice=True)
4197
4198                # Widen raw data with UTC current time (iData["actualDateTime"]):
4199                actualDate = datetime.now(tzutc())
4200                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4201
4202                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4203                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4204
4205                # Replace some values with human-readable:
4206                iData["nominalCurrency"] = iData["nominal"]["currency"]
4207                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4208                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4209                iData["aciCurrency"] = iData["aciValue"]["currency"]
4210                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4211                iData["issueSize"] = int(iData["issueSize"])
4212                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4213                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4214                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4215                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4216                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4217                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4218                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4219                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4220                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4221                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4222
4223                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4224                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4225                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4226                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4227                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4228                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4229                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4230                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4231                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4232                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4233                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4234
4235                # Widen raw data with calendar data from `rawCalendar` values:
4236                calendarData = []
4237                if "events" in iData["rawCalendar"].keys():
4238                    for item in iData["rawCalendar"]["events"]:
4239                        calendarData.append({
4240                            "couponDate": item["couponDate"],
4241                            "couponNumber": int(item["couponNumber"]),
4242                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4243                            "payCurrency": item["payOneBond"]["currency"],
4244                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4245                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4246                            "couponStartDate": item["couponStartDate"],
4247                            "couponEndDate": item["couponEndDate"],
4248                            "couponPeriod": item["couponPeriod"],
4249                        })
4250
4251                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4252                    if "maturityDate" not in iData.keys():
4253                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4254
4255                # Widen raw data with Coupon Rate.
4256                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4257                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4258                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4259                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4260
4261                # Widen raw data with Yield to Maturity (YTM) on current date.
4262                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4263                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4264                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4265                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4266                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4267                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4268
4269                iData["calendar"] = calendarData  # adds calendar at the end
4270
4271                # Remove not used data:
4272                iData.pop("uid")
4273                iData.pop("positionUid")
4274                iData.pop("currentPrice")
4275                iData.pop("rawCalendar")
4276
4277                colNames = list(iData.keys())
4278                if bonds is None:
4279                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4280
4281                else:
4282                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4283
4284            else:
4285                uLogger.warning("Instrument is not a bond!")
4286
4287            processed = round(100 * (i + 1) / iCount, 1)
4288            if tooLong and processed % 5 == 0:
4289                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4290
4291            else:
4292                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4293
4294        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4295
4296        # Saving bonds from Pandas DataFrame to XLSX sheet:
4297        if xlsx and self.bondsXLSXFile:
4298            with pd.ExcelWriter(
4299                    path=self.bondsXLSXFile,
4300                    date_format=TKS_DATE_FORMAT,
4301                    datetime_format=TKS_DATE_TIME_FORMAT,
4302                    mode="w",
4303            ) as writer:
4304                bonds.to_excel(
4305                    writer,
4306                    sheet_name="Extended bonds data",
4307                    index=True,
4308                    encoding="UTF-8",
4309                    freeze_panes=(1, 1),
4310                )  # saving as XLSX-file with freeze first row and column as headers
4311
4312            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4313
4314        return bonds

Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • xlsx: if True then also exports Pandas DataFrame to xlsx-file bondsXLSXFile, default ext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns

wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon

def CreateBondsCalendar( self, extBonds: pandas.core.frame.DataFrame, xlsx: bool = False) -> pandas.core.frame.DataFrame:
4316    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4317        """
4318        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4319
4320        WARNING! This is too long operation if a lot of bonds requested from broker server.
4321
4322        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4323
4324        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4325                        extended information about bonds: main info, current prices, bond payment calendar,
4326                        coupon yields, current yields and some statistics etc.
4327                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4328        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4329                     for further used by data scientists or stock analytics.
4330        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4331        """
4332        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4333            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4334
4335        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4336
4337        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4338        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4339        calendar = None
4340        for bond in extBonds.iterrows():
4341            for item in bond[1]["calendar"]:
4342                cData = {
4343                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4344                    "couponDate": item["couponDate"],
4345                    "figi": bond[1]["figi"],
4346                    "ticker": bond[1]["ticker"],
4347                    "name": bond[1]["name"],
4348                    "couponNumber": item["couponNumber"],
4349                    "payOneBond": item["payOneBond"],
4350                    "payCurrency": item["payCurrency"],
4351                    "couponType": item["couponType"],
4352                    "couponPeriod": item["couponPeriod"],
4353                    "fixDate": item["fixDate"],
4354                    "couponStartDate": item["couponStartDate"],
4355                    "couponEndDate": item["couponEndDate"],
4356                }
4357
4358                if calendar is None:
4359                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4360
4361                else:
4362                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4363
4364        if calendar is not None:
4365            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4366
4367            # Saving calendar from Pandas DataFrame to XLSX sheet:
4368            if xlsx:
4369                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4370
4371                with pd.ExcelWriter(
4372                        path=xlsxCalendarFile,
4373                        date_format=TKS_DATE_FORMAT,
4374                        datetime_format=TKS_DATE_TIME_FORMAT,
4375                        mode="w",
4376                ) as writer:
4377                    humanReadable = calendar.copy(deep=True)
4378                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4379                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4380                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4381                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4382                    humanReadable.columns = colNames  # human-readable column names
4383
4384                    humanReadable.to_excel(
4385                        writer,
4386                        sheet_name="Bond payments calendar",
4387                        index=False,
4388                        encoding="UTF-8",
4389                        freeze_panes=(1, 2),
4390                    )  # saving as XLSX-file with freeze first row and column as headers
4391
4392                    del humanReadable  # release df in memory
4393
4394                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4395
4396        return calendar

Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowBondsCalendar(), ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • xlsx: if True then also exports Pandas DataFrame to file calendarFile + ".xlsx", calendar.xlsx by default, for further used by data scientists or stock analytics.
Returns

Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon

def ShowBondsCalendar(self, extBonds: pandas.core.frame.DataFrame, show: bool = True) -> str:
4398    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4399        """
4400        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4401        Also, creates Markdown file with calendar data, `calendar.md` by default.
4402
4403        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4404
4405        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4406                        extended information about bonds: main info, current prices, bond payment calendar,
4407                        coupon yields, current yields and some statistics etc.
4408                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4409        :param show: if `True` then also printing bonds payment calendar to the console,
4410                     otherwise save to file `calendarFile` only. `False` by default.
4411        :return: multilines text in Markdown format with bonds payment calendar as a table.
4412        """
4413        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4414            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4415
4416        infoText = "# Bond payments calendar\n\n"
4417
4418        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4419
4420        if not (calendar is None or calendar.empty):
4421            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4422
4423            info = [
4424                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4425                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4426                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4427            ]
4428
4429            newMonth = False
4430            notOneBond = calendar["figi"].nunique() > 1
4431            for i, bond in enumerate(calendar.iterrows()):
4432                if newMonth and notOneBond:
4433                    info.append(splitLine)
4434
4435                info.append(
4436                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4437                        "  √" if bond[1]["paid"] else "  —",
4438                        bond[1]["couponDate"].split("T")[0],
4439                        bond[1]["figi"],
4440                        bond[1]["ticker"],
4441                        bond[1]["couponNumber"],
4442                        "{} {}".format(
4443                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4444                            bond[1]["payCurrency"],
4445                        ),
4446                        bond[1]["couponType"],
4447                        bond[1]["couponPeriod"],
4448                        bond[1]["fixDate"].split("T")[0],
4449                    )
4450                )
4451
4452                if i < len(calendar.values) - 1:
4453                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4454                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4455                    newMonth = False if curDate.month == nextDate.month else True
4456
4457                else:
4458                    newMonth = False
4459
4460            infoText += "".join(info)
4461
4462            if show:
4463                uLogger.info("{}".format(infoText))
4464
4465            if self.calendarFile is not None:
4466                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4467                    fH.write(infoText)
4468
4469                uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4470
4471                if self.useHTMLReports:
4472                    htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html"
4473                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4474                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText))
4475
4476                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4477
4478        else:
4479            infoText += "No data\n"
4480
4481        return infoText

Show bond payments calendar as a table. One row in input bonds dataframe contains one bond. Also, creates Markdown file with calendar data, calendar.md by default.

See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • show: if True then also printing bonds payment calendar to the console, otherwise save to file calendarFile only. False by default.
Returns

multilines text in Markdown format with bonds payment calendar as a table.

def OverviewAccounts(self, show: bool = False) -> dict:
4483    def OverviewAccounts(self, show: bool = False) -> dict:
4484        """
4485        Method for parsing and show simple table with all available user accounts.
4486
4487        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4488
4489        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4490        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4491                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4492                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4493                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4494                                                        "closed": "—", "access": "Full access" }, ...}}`
4495        """
4496        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4497
4498        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4499        accounts = {
4500            item["id"]: {
4501                "type": TKS_ACCOUNT_TYPES[item["type"]],
4502                "name": item["name"],
4503                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4504                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4505                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4506                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4507            } for item in rawAccounts["accounts"]
4508        }
4509
4510        # Raw and parsed data with some fields replaced in "stat" section:
4511        view = {
4512            "rawAccounts": rawAccounts,
4513            "stat": accounts,
4514        }
4515
4516        # --- Prepare simple text table with only accounts data in human-readable format:
4517        if show:
4518            info = [
4519                "# User accounts\n\n",
4520                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4521                "| Account ID   | Type                      | Status                    | Name                           |\n",
4522                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4523            ]
4524
4525            for account in view["stat"].keys():
4526                info.extend([
4527                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4528                        account,
4529                        view["stat"][account]["type"],
4530                        view["stat"][account]["status"],
4531                        view["stat"][account]["name"],
4532                    )
4533                ])
4534
4535            infoText = "".join(info)
4536
4537            uLogger.info(infoText)
4538
4539            if self.userAccountsFile:
4540                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4541                    fH.write(infoText)
4542
4543                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4544
4545                if self.useHTMLReports:
4546                    htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html"
4547                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4548                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText))
4549
4550                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4551
4552        return view

Method for parsing and show simple table with all available user accounts.

See also: RequestAccounts() and OverviewUserInfo() methods.

Parameters
  • show: if False then only dictionary with accounts data returns, if True then also print it to log.
Returns

dict with parsed accounts data received from RequestAccounts() method. Example of dict: view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}

def OverviewUserInfo(self, show: bool = False) -> dict:
4554    def OverviewUserInfo(self, show: bool = False) -> dict:
4555        """
4556        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4557
4558        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4559
4560        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4561        :return: dict with raw parsed data from server and some calculated statistics about it.
4562        """
4563        overview = self.Overview(show=False)  # Request current user portfolio for the ability to calculate missing funds
4564        tmpTicker = self._ticker
4565        self._ticker = "RUB000UTSTOM"  # This instrument show in rub how much money cost current margin
4566        missing = self.GetInstrumentFromPortfolio(portfolio=overview)
4567        self._ticker = tmpTicker
4568
4569        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4570        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4571        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4572        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4573        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4574        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4575
4576        # This is dict with parsed common user data:
4577        userInfo = {
4578            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4579            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4580            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4581            "tariff": rawUserInfo["tariff"],
4582        }
4583
4584        # This is an array of dict with parsed margin statuses for every account IDs:
4585        margins = {}
4586        for accountId in accounts.keys():
4587            if rawMargins[accountId]:
4588                margins[accountId] = {
4589                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4590                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4591                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4592                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4593                    "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4594                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4595                    "missing": missing["volume"],
4596                }
4597
4598            else:
4599                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4600
4601        unary = {}  # unary-connection limits
4602        for item in rawTariffLimits["unaryLimits"]:
4603            if item["limitPerMinute"] in unary.keys():
4604                unary[item["limitPerMinute"]].extend(item["methods"])
4605
4606            else:
4607                unary[item["limitPerMinute"]] = item["methods"]
4608
4609        stream = {}  # stream-connection limits
4610        for item in rawTariffLimits["streamLimits"]:
4611            if item["limit"] in stream.keys():
4612                stream[item["limit"]].extend(item["streams"])
4613
4614            else:
4615                stream[item["limit"]] = item["streams"]
4616
4617        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4618        limits = {
4619            "unary": unary,
4620            "stream": stream,
4621        }
4622
4623        # Raw and parsed data as an output result:
4624        view = {
4625            "rawUserInfo": rawUserInfo,
4626            "rawAccounts": rawAccounts,
4627            "rawMargins": rawMargins,
4628            "rawTariffLimits": rawTariffLimits,
4629            "stat": {
4630                "overview": overview,
4631                "userInfo": userInfo,
4632                "accounts": accounts,
4633                "margins": margins,
4634                "limits": limits,
4635            },
4636        }
4637
4638        # --- Prepare text table with user information in human-readable format:
4639        if show:
4640            info = [
4641                "# Full user information\n\n",
4642                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4643                "## Common information\n\n",
4644                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4645                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4646                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4647                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4648                "\n## User accounts\n\n",
4649            ]
4650
4651            for account in view["stat"]["accounts"].keys():
4652                info.extend([
4653                    "### ID: [{}]\n\n".format(account),
4654                    "| Parameters           | Values                                                       |\n",
4655                    "|----------------------|--------------------------------------------------------------|\n",
4656                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4657                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4658                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4659                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4660                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4661                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4662                ])
4663
4664                if margins[account]:
4665                    info.extend([
4666                        "| Margin status:       | Enabled                                                      |\n",
4667                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4668                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4669                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4670                        "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])),
4671                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4672                        "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])),
4673                    ])
4674
4675                else:
4676                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4677
4678            info.extend([
4679                "\n## Current user tariff limits\n",
4680                "\n### See also\n",
4681                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4682                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4683                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4684                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4685                "\n### Unary limits\n",
4686            ])
4687
4688            if unary:
4689                for key, values in sorted(unary.items()):
4690                    info.append("\n* Max requests per minute: {}\n".format(key))
4691
4692                    for value in values:
4693                        info.append("  - {}\n".format(value))
4694
4695            else:
4696                info.append("\nNot available\n")
4697
4698            info.append("\n### Stream limits\n")
4699
4700            if stream:
4701                for key, values in sorted(stream.items()):
4702                    info.append("\n* Max stream connections: {}\n".format(key))
4703
4704                    for value in values:
4705                        info.append("  - {}\n".format(value))
4706
4707            else:
4708                info.append("\nNot available\n")
4709
4710            infoText = "".join(info)
4711
4712            uLogger.info(infoText)
4713
4714            if self.userInfoFile:
4715                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4716                    fH.write(infoText)
4717
4718                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4719
4720                if self.useHTMLReports:
4721                    htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html"
4722                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4723                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText))
4724
4725                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4726
4727        return view

Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).

See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.

Parameters
  • show: if False then only dictionary returns, if True then also print user's data to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

class Args:
4730class Args:
4731    """
4732    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4733    """
4734    def __init__(self, **kwargs):
4735        self.__dict__.update(kwargs)
4736
4737    def __getattr__(self, item):
4738        return None

If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.

Args(**kwargs)
4734    def __init__(self, **kwargs):
4735        self.__dict__.update(kwargs)
def ParseArgs():
4741def ParseArgs():
4742    """This function get and parse command line keys."""
4743    parser = ArgumentParser()  # command-line string parser
4744
4745    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4746    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4747
4748    # --- options:
4749
4750    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4751    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4752    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4753
4754    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4755    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4756
4757    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4758    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4759
4760    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4761    parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.")
4762
4763    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4764    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4765    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4766
4767    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4768    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4769
4770    # --- commands:
4771
4772    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4773
4774    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4775    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4776    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4777    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4778    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4779    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4780    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4781    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4782
4783    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4784    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4785    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4786    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4787    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4788    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4789
4790    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4791    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4792    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4793    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4794
4795    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4796    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4797    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4798
4799    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4800    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4801    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4802    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4803    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4804    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4805    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4806
4807    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4808    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4809    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4810    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4811    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.")
4812
4813    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4814    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4815    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4816
4817    cmdArgs = parser.parse_args()
4818    return cmdArgs

This function get and parse command line keys.

def Main(**kwargs):
4821def Main(**kwargs):
4822    """
4823    Main function for work with TKSBrokerAPI in the console.
4824
4825    See examples:
4826    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4827    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4828    """
4829    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4830
4831    if args.debug_level:
4832        uLogger.level = 10  # always debug level by default
4833        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4834
4835    exitCode = 0
4836    start = datetime.now(tzutc())
4837    uLogger.debug("=-" * 50)
4838    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4839        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4840        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4841    ))
4842
4843    # trying to calculate full current version:
4844    buildVersion = __version__
4845    try:
4846        v = version("tksbrokerapi")
4847        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4848
4849    except Exception:
4850        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4851
4852    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4853    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4854
4855    try:
4856        if args.version:
4857            print("TKSBrokerAPI {}".format(buildVersion))
4858            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4859
4860        else:
4861            # Init class for trading with Tinkoff Broker:
4862            trader = TinkoffBrokerServer(
4863                token=args.token,
4864                accountId=args.account_id,
4865                useCache=not args.no_cache,
4866            )
4867
4868            # --- set some options:
4869
4870            if args.more:
4871                trader.moreDebug = True
4872                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4873
4874            if args.html:
4875                trader.useHTMLReports = True
4876
4877            if args.ticker:
4878                ticker = str(args.ticker).upper()  # Tickers may be upper case only
4879
4880                if ticker in trader.aliasesKeys:
4881                    trader.ticker = trader.aliases[ticker]  # Replace some tickers with its aliases
4882
4883                else:
4884                    trader.ticker = ticker
4885
4886            if args.figi:
4887                trader.figi = str(args.figi).upper()  # FIGIs may be upper case only
4888
4889            if args.depth is not None:
4890                trader.depth = args.depth
4891
4892            # --- do one command:
4893
4894            if args.list:
4895                if args.output is not None:
4896                    trader.instrumentsFile = args.output
4897
4898                trader.ShowInstrumentsInfo(show=True)
4899
4900            elif args.list_xlsx:
4901                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4902
4903            elif args.bonds_xlsx is not None:
4904                if args.output is not None:
4905                    trader.bondsXLSXFile = args.output
4906
4907                if len(args.bonds_xlsx) == 0:
4908                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4909
4910                else:
4911                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4912
4913            elif args.search:
4914                if args.output is not None:
4915                    trader.searchResultsFile = args.output
4916
4917                trader.SearchInstruments(pattern=args.search[0], show=True)
4918
4919            elif args.info:
4920                if not (args.ticker or args.figi):
4921                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4922                    raise Exception("Ticker or FIGI required")
4923
4924                if args.output is not None:
4925                    trader.infoFile = args.output
4926
4927                if args.ticker:
4928                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4929
4930                else:
4931                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4932
4933            elif args.calendar is not None:
4934                if args.output is not None:
4935                    trader.calendarFile = args.output
4936
4937                if len(args.calendar) == 0:
4938                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4939
4940                else:
4941                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4942
4943                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4944
4945            elif args.price:
4946                if not (args.ticker or args.figi):
4947                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4948                    raise Exception("Ticker or FIGI required")
4949
4950                trader.GetCurrentPrices(show=True)
4951
4952            elif args.prices is not None:
4953                if args.output is not None:
4954                    trader.pricesFile = args.output
4955
4956                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4957
4958            elif args.overview:
4959                if args.output is not None:
4960                    trader.overviewFile = args.output
4961
4962                trader.Overview(show=True, details="full")
4963
4964            elif args.overview_digest:
4965                if args.output is not None:
4966                    trader.overviewDigestFile = args.output
4967
4968                trader.Overview(show=True, details="digest")
4969
4970            elif args.overview_positions:
4971                if args.output is not None:
4972                    trader.overviewPositionsFile = args.output
4973
4974                trader.Overview(show=True, details="positions")
4975
4976            elif args.overview_orders:
4977                if args.output is not None:
4978                    trader.overviewOrdersFile = args.output
4979
4980                trader.Overview(show=True, details="orders")
4981
4982            elif args.overview_analytics:
4983                if args.output is not None:
4984                    trader.overviewAnalyticsFile = args.output
4985
4986                trader.Overview(show=True, details="analytics")
4987
4988            elif args.overview_calendar:
4989                if args.output is not None:
4990                    trader.overviewAnalyticsFile = args.output
4991
4992                trader.Overview(show=True, details="calendar")
4993
4994            elif args.deals is not None:
4995                if args.output is not None:
4996                    trader.reportFile = args.output
4997
4998                if 0 <= len(args.deals) < 3:
4999                    trader.Deals(
5000                        start=args.deals[0] if len(args.deals) >= 1 else None,
5001                        end=args.deals[1] if len(args.deals) == 2 else None,
5002                        show=True,  # Always show deals report in console
5003                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
5004                    )
5005
5006                else:
5007                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5008                    raise Exception("Incorrect value")
5009
5010            elif args.history is not None:
5011                if args.output is not None:
5012                    trader.historyFile = args.output
5013
5014                if 0 <= len(args.history) < 3:
5015                    dataReceived = trader.History(
5016                        start=args.history[0] if len(args.history) >= 1 else None,
5017                        end=args.history[1] if len(args.history) == 2 else None,
5018                        interval="hour" if args.interval is None or not args.interval else args.interval,
5019                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
5020                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
5021                        show=True,  # shows all downloaded candles in console
5022                    )
5023
5024                    if args.render_chart is not None and dataReceived is not None:
5025                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5026
5027                        trader.ShowHistoryChart(
5028                            candles=dataReceived,
5029                            interact=iChart,
5030                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5031                        )
5032
5033                else:
5034                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5035                    raise Exception("Incorrect value")
5036
5037            elif args.load_history is not None:
5038                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
5039
5040                if args.render_chart is not None and histData is not None:
5041                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5042                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
5043
5044                    trader.ShowHistoryChart(
5045                        candles=histData,
5046                        interact=iChart,
5047                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5048                    )
5049
5050            elif args.trade is not None:
5051                if 1 <= len(args.trade) <= 5:
5052                    trader.Trade(
5053                        operation=args.trade[0],
5054                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
5055                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
5056                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
5057                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
5058                    )
5059
5060                else:
5061                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5062
5063            elif args.buy is not None:
5064                if 0 <= len(args.buy) <= 4:
5065                    trader.Buy(
5066                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
5067                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
5068                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
5069                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
5070                    )
5071
5072                else:
5073                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5074
5075            elif args.sell is not None:
5076                if 0 <= len(args.sell) <= 4:
5077                    trader.Sell(
5078                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
5079                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
5080                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
5081                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
5082                    )
5083
5084                else:
5085                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5086
5087            elif args.order:
5088                if 4 <= len(args.order) <= 7:
5089                    trader.Order(
5090                        operation=args.order[0],
5091                        orderType=args.order[1],
5092                        lots=int(args.order[2]),
5093                        targetPrice=float(args.order[3]),
5094                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
5095                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
5096                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
5097                    )
5098
5099                else:
5100                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
5101
5102            elif args.buy_limit:
5103                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
5104
5105            elif args.sell_limit:
5106                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
5107
5108            elif args.buy_stop:
5109                if 2 <= len(args.buy_stop) <= 7:
5110                    trader.BuyStop(
5111                        lots=int(args.buy_stop[0]),
5112                        targetPrice=float(args.buy_stop[1]),
5113                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
5114                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
5115                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
5116                    )
5117
5118                else:
5119                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5120
5121            elif args.sell_stop:
5122                if 2 <= len(args.sell_stop) <= 7:
5123                    trader.SellStop(
5124                        lots=int(args.sell_stop[0]),
5125                        targetPrice=float(args.sell_stop[1]),
5126                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
5127                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
5128                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
5129                    )
5130
5131                else:
5132                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
5133
5134            # elif args.buy_order_grid is not None:
5135            #     # update order grid work with api v2
5136            #     if len(args.buy_order_grid) == 2:
5137            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
5138            #
5139            #         for order in orderParams:
5140            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
5141            #
5142            #     else:
5143            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5144            #
5145            # elif args.sell_order_grid is not None:
5146            #     # update order grid work with api v2
5147            #     if len(args.sell_order_grid) >= 2:
5148            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
5149            #
5150            #         for order in orderParams:
5151            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
5152            #
5153            #     else:
5154            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5155
5156            elif args.close_order is not None:
5157                trader.CloseOrders(args.close_order)  # close only one order
5158
5159            elif args.close_orders is not None:
5160                trader.CloseOrders(args.close_orders)  # close list of orders
5161
5162            elif args.close_trade:
5163                if not (args.ticker or args.figi):
5164                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5165                    raise Exception("Ticker or FIGI required")
5166
5167                if args.ticker:
5168                    trader.CloseTrades([str(args.ticker).upper()])  # close only one trade by ticker (priority)
5169
5170                else:
5171                    trader.CloseTrades([str(args.figi).upper()])  # close only one trade by FIGI
5172
5173            elif args.close_trades is not None:
5174                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
5175
5176            elif args.close_all is not None:
5177                if args.ticker:
5178                    trader.CloseAllByTicker(instrument=str(args.ticker).upper())
5179
5180                elif args.figi:
5181                    trader.CloseAllByFIGI(instrument=str(args.figi).upper())
5182
5183                else:
5184                    trader.CloseAll(*args.close_all)
5185
5186            elif args.limits:
5187                if args.output is not None:
5188                    trader.withdrawalLimitsFile = args.output
5189
5190                trader.OverviewLimits(show=True)
5191
5192            elif args.user_info:
5193                if args.output is not None:
5194                    trader.userInfoFile = args.output
5195
5196                trader.OverviewUserInfo(show=True)
5197
5198            elif args.account:
5199                if args.output is not None:
5200                    trader.userAccountsFile = args.output
5201
5202                trader.OverviewAccounts(show=True)
5203
5204            else:
5205                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
5206                raise Exception("There is no command to execute")
5207
5208    except Exception:
5209        trace = tb.format_exc()
5210        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
5211            if e in trace:
5212                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
5213                break
5214
5215        uLogger.debug(trace)
5216        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
5217        exitCode = 255  # an error occurred, must be open a ticket for this issue
5218
5219    finally:
5220        finish = datetime.now(tzutc())
5221
5222        if exitCode == 0:
5223            if args.more:
5224                uLogger.debug("All operations were finished success (summary code is 0).")
5225
5226        else:
5227            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
5228                os.path.abspath(uLog.defaultLogFile), exitCode,
5229            ))
5230
5231        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
5232        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
5233            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
5234            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
5235        ))
5236        uLogger.debug("=-" * 50)
5237
5238        if not kwargs:
5239            sys.exit(exitCode)
5240
5241        else:
5242            return exitCode

Main function for work with TKSBrokerAPI in the console.

See examples: